- Docs
- Tutorials
- Build a booking API
Build a booking API
Run the examples/bookings app end to end: migrate, seed, serve, start the worker, and book a space over HTTP.
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.
cd examples/bookingsRead the schema
Section titled “Read the schema”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.
cat schema/bookings.zenMigrate the database
Section titled “Migrate the database”Generated code is committed under generated/; never hand-edit it, regenerate with zever compile. The migration creates the sqlite database:
zever db migrate --adapter=sqlite --dsn=data/bookings.db schema/bookings.zenCheckpoint:
OK applied 8 statement(s) via sqliteIf you run zever doctor from another directory, the db check reports a path miss, so run it from here.
Seed demo data
Section titled “Seed demo data”The seed is idempotent:
go run ./db/seedCheckpoint:
{"time":"...","level":"INFO","msg":"seed complete"}It creates host@example.com and guest@example.com, two spaces, and one confirmed booking.
Set a JWT secret and serve
Section titled “Set a JWT secret and serve”The app fails closed when AUTH_JWT_SECRET is missing or under 32 bytes (Configuration). Export one (never commit a real one):
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-longgo run ./cmd/serverCheckpoint (-addr changes the port):
{"time":"...","level":"INFO","msg":"http server listening","addr":":8080"}Start the worker
Section titled “Start the worker”In a second terminal, export the same secret and start the worker (jobs plus scheduler):
export AUTH_JWT_SECRET=dev-only-secret-at-least-32-bytes-longgo run ./cmd/workerCheckpoint:
{"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.
Call the API
Section titled “Call the API”Register a guest:
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:
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:
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":"..."}]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).
Trace one booking through the stack
Section titled “Trace one booking through the stack”A successful booking request passes, in order, through:
auth: the bearer JWT is verified and attached to the context.ratelimit: the per-guest quota is checked; over quota returns 429.idempotency: a retried POST with the sameIdempotency-Keyheader (oridempotency_keybody field, or the same guest, space, and dates) replays the first result.lock: the space date range is held so overlapping bookings cannot both confirm (409dates already booked).payment: a stub charge (no real funds), then the booking row is saved asconfirmed.queue: abookings.confirmmessage is pushed. Thememoryqueue is per process, so the separate worker does not receive it; setqueueto a shared adapter such asredisin both processes to hand work across.
Understand the doctor failures
Section titled “Understand the doctor failures”zever doctorExpect 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.
What you built
Section titled “What you built”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.