Skip to content
Zever
Home

Build a booking API

Run the examples/bookings app end to end: migrate, seed, serve, start the worker, and book a space over HTTP.

3 min readEdit on GitHub

You will run examples/bookings, a space marketplace where hosts list spaces and guests book them. It takes about 15 minutes. You need Go 1.27+, the zever CLI, and a repo clone.

All commands run from examples/bookings/ so .env and the relative data/ path resolve.

Terminal window
cd examples/bookings

The whole domain lives in one file: the User, Space, Booking, and Review entities, the BookingService RPCs, two jobs (SendConfirmation, SendReminder), and a Reminder cron schedule. See Zen syntax to extend it.

Terminal window
cat schema/bookings.zen

Generated code is committed under generated/; never hand-edit it, regenerate with zever compile. The migration creates the sqlite database:

Terminal window
zever db migrate --adapter=sqlite --dsn=data/bookings.db schema/bookings.zen

Checkpoint:

OK applied 8 statement(s) via sqlite

If you run zever doctor from another directory, the db check reports a path miss, so run it from here.

The seed is idempotent:

Terminal window
go run ./db/seed

Checkpoint:

{"time":"...","level":"INFO","msg":"seed complete"}

It creates host@example.com and guest@example.com, two spaces, and one confirmed booking.

The app fails closed when AUTH_JWT_SECRET is missing or under 32 bytes (Configuration). Export one (never commit a real one):

Terminal window
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-long
go run ./cmd/server

Checkpoint (-addr changes the port):

{"time":"...","level":"INFO","msg":"http server listening","addr":":8080"}

In a second terminal, export the same secret and start the worker (jobs plus scheduler):

Terminal window
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-long
go run ./cmd/worker

Checkpoint:

{"time":"...","level":"INFO","msg":"worker started","jobs":2,"schedules":1}

The worker consumes the default and low queues and fires the Reminder cron (0 9 * * *). SendConfirmation sends a mail receipt (mailer/log), a guest push (notification/log), and a booking.created webhook to any configured target (skipped quietly when none; the app README lists BOOKINGS_WEBHOOK_*). SendReminder notifies guests of confirmed bookings starting within 7 days.

Register a guest:

Terminal window
curl -s -X POST localhost:8080/api/register \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"correct horse battery staple"}'
{"id":"01J00000000000000000000000","email":"you@example.com"}

Log in. The response holds a JWT for every other route:

Terminal window
curl -s -X POST localhost:8080/api/login \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"correct horse battery staple"}'
{"token":"eyJhbGciOi..."}

List spaces, then book one:

Terminal window
TOKEN=... # from the login response
curl -s localhost:8080/api/spaces -H "Authorization: Bearer $TOKEN"
[{"id":"...","host_id":"...","title":"...","price_cents":...},{"id":"...","host_id":"...","title":"..."}]
Terminal window
curl -s -X POST localhost:8080/api/bookings \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"space_id":"...","start_date":"2026-01-01","end_date":"2026-01-03"}'
{"booking":{"id":"...","space_id":"...","status":"confirmed",...},"message":"...","payment_id":"..."}

The response is HTTP 201. Repeat the same request and you get the first result back with HTTP 200 (idempotent replay).

A successful booking request passes, in order, through:

  1. auth: the bearer JWT is verified and attached to the context.
  2. ratelimit: the per-guest quota is checked; over quota returns 429.
  3. idempotency: a retried POST with the same Idempotency-Key header (or idempotency_key body field, or the same guest, space, and dates) replays the first result.
  4. lock: the space date range is held so overlapping bookings cannot both confirm (409 dates already booked).
  5. payment: a stub charge (no real funds), then the booking row is saved as confirmed.
  6. queue: a bookings.confirm message is pushed. The memory queue is per process, so the separate worker does not receive it; set queue to a shared adapter such as redis in both processes to hand work across.
Terminal window
zever doctor

Expect 30 OK and 4 FAIL: ai (no API key), auth (no JWT secret), geo (missing fixture data), and i18n (missing embed FS). These are deliberate zero-infra gaps, not framework bugs; the app README.md lists them.

One HTTP request touched auth, rate limiting, idempotency, locking, payment, the ORM, and the queue, while the worker ran the job and schedule handlers.

Troubleshooting

The server exits immediately naming AUTH_JWT_SECRET. The secret is missing or under 32 bytes. Export a longer dev-only value.

curl returns 401 on /api/spaces. The Authorization: Bearer $TOKEN header is missing, or the token came from a different secret. Log in again in the terminal where you exported the secret.

A booking returns 429 or 409. 429 means the rate limit bit; wait out the window. 409 means the dates overlap an existing booking, or the same booking is still in progress.