Skip to content
BoringStack
Star

API template: overview

3 min read

The API layer owns security, data, and background work: auth, sessions, OAuth, email, queues, audit log, Stripe billing, structured logging, and an env validator that refuses to boot if anything is missing.

apps/api/ is built on Bun (runtime), Elysia (HTTP framework), Drizzle (ORM), Postgres (durable state), Valkey (cache/queues), and BullMQ (background jobs). A feature folder splits into routes (HTTP + TypeBox validation), service (logic + database queries), and types (shared interfaces). Lint rules forbid cross-imports that would blur the split.

Per-feature folders contain one concern each. Adding product behavior means one folder, not scattered route/service/model edits. Routes, services, and types stay separate by lint rule: a *.routes.ts that imports drizzle-orm fails the build, and a *.service.ts that imports Elysia’s t does too. The split survives refactors and agent-written code.

Config is frozen at boot. process.env is read in one validator; misconfigured deployments fail before serving traffic. Infrastructure is pluggable: email, AI, cache, and queues swap through config. Dev runs without vendor keys.

Drizzle provides TS-first models with SQL-shaped schema and real migration files. OpenAPI emits the UI boundary: the React app calls a generated client, so server changes become type errors instead of runtime surprises.

src/ contains:

  • index.ts: Entrypoint (env, Sentry, queues, listen)
  • config/: App composition, env, logger, queue bootstrap
  • api/: Feature folders (auth, users, accounts, dashboard, billing, admin, health, notifications)
  • clients/postgres/: Drizzle client and per-domain schema modules
  • lib/: Shared utilities (auth, email, audit-log, AI, cache, errors, notifications)
  • middleware/: Per-route Elysia plugins
  • queues/: BullMQ queue and worker pairs
  • templates/email/: Handlebars sources, compiled to JSON at build

A feature folder (e.g., src/api/posts/) always has:

  • posts.routes.ts: HTTP surface
  • posts.service.ts: Business logic and database queries
  • posts.types.ts: Shapes shared between routes and service
  • posts.schemas.ts (optional): TypeBox request/response validation

The shipped modules (auth, users, accounts, billing, dashboard, admin, health, notifications) are framework infrastructure. api/ is yours to fill with domain resources. The account-scoped resource pattern is enforced by the drizzle-conventions/account-scoped-tables-require-where lint rule, so every new account-scoped resource inherits isolation for free. Add new resources with bun run new:resource <name>; the scaffolder writes the anatomy and wires it into config/routes.ts. The First feature in 10 minutes tutorial walks the full backend and UI loop.

  • Auth: Cookie sessions, short-lived access JWT cookies, DB-backed refresh sessions. See Authentication.
  • Tenant: Accounts are the tenant boundary; users join via memberships with roles. See Multi-tenant model.
  • ACL: Server-authoritative CASL ability, plan and feature gates. See ACL & feature resolution.
  • Billing: Stripe Checkout, Customer Portal, raw-body webhooks, DB-backed idempotency. See Billing.
  • Email: Pluggable provider (Cloudflare, Resend, SendGrid, SMTP), precompiled templates, queue-aware dispatch. See Email.
  • Queues: BullMQ with QueueManager and inline fallback. See Queues.
  • Audit: Fire-and-forget append-only event log. See Audit log.
  • Env: TypeBox shape plus hand-written invariants at boot. See Env validator.

The architecture is held in place by a family of custom ESLint plugins. bun run validate is the merge gate. See Lint as the contract for why these rules matter.