Build a full-stack app

View as Markdown

This tutorial deploys the acme app end to end: a Vite front end (web) and a Go API (api), joined by a mount so that /api/* on the front end forwards to the API over in-process dispatch. One hyjal.toml describes both Resources; one hyjal deploy ships them together.

Time: about twenty minutes. You need the hyjal CLI, a logged-in session, Node.js, and TinyGo for the Go lane.

The shape

The front end is public. The API is private and reachable only through the mount. A request to https://web--acme.hyjal.cloud/api/health lands on web, matches the /api/* mount, and dispatches to api in-process: same origin, no CORS, requests traced end to end.

1. Lay out the repository

acme/
hyjal.toml
web/ # Vite front end
api/ # Go API

2. Build the front end

Scaffold the front end under web/ and add a call to the mounted API.

1// web/src/main.js
2const res = await fetch("/api/health");
3const data = await res.json();
4document.querySelector("#status").textContent = data.status;

The front end calls /api/health on its own origin. The mount handles routing; the browser never sees a cross-origin request.

3. Build the Go API

Write the API under api/ as a wasi:http handler.

1// api/main.go
2package main
3
4import (
5 "encoding/json"
6 "net/http"
7
8 "go.wasmcloud.dev/component/net/wasihttp"
9)
10
11func handler(w http.ResponseWriter, r *http.Request) {
12 w.Header().Set("content-type", "application/json")
13 json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
14}
15
16func main() {
17 wasihttp.HandleFunc(handler)
18}

4. Write the manifest

Declare both Resources in one manifest. The mounts field on web forwards the /api/* prefix to api. Marking api as public = false keeps it off the public internet; it is reachable only through the mount.

1[project]
2name = "acme"
3
4[resource.web]
5lane = "static"
6build = "npm run build"
7output = "dist"
8workdir = "web"
9spa = true
10mounts = { "/api/*" = "api" }
11
12[resource.api]
13lane = "go"
14public = false
15workdir = "api"
16run_limit_secs = 30

The workdir field points each Resource at its subdirectory. See Monorepos for larger layouts.

5. Deploy both Resources

hyjal deploy builds every Resource, uploads both artifacts, applies policy, and flips routes atomically. The front end and API go live together.

$$ hyjal deploy
$Building resource web (lane: static, workdir: web)...
$ npm run build
$ compressing assets (brotli, gzip) ✓
$Building resource api (lane: go, workdir: api)...
$ tinygo build -target=wasip2
$Uploading artifacts...
$ web digest sha256:4d0b8a…1f77
$ api digest sha256:e2c96b…a530
$Applying policy...
$Flipping routes...
$
$Deployed acme
$ web https://web--acme.hyjal.cloud
$ api (private, mounted at web:/api/*)

6. Verify the mount

Call the front end, then the mounted API path on the same origin:

$$ curl https://web--acme.hyjal.cloud/api/health
${"status":"ok"}

The API responds through the front-end origin. A direct request to the private API endpoint is refused, because api is not public:

$$ curl -i https://api--acme.hyjal.cloud/health
$HTTP/2 404

7. Trace a request end to end

Reload the front end and stream logs. The request to web and the dispatched call to api are correlated.

$$ hyjal logs -f
$[16:41:02] web GET / 200 8ms
$[16:41:02] web GET /api/health 200 3ms
$[16:41:02] api GET /health 200 1ms

Drawing an edge between web and api on the Board configures the same mount visually.

What you built

  • The acme app: a public web front end and a private api Go service.
  • A /api/* mount joining them over same-origin in-process dispatch.
  • One manifest and one deploy that ship both Resources atomically.

Next steps