Skip to content
BoringStack
GitHub

API template: overview

5 min read

apps/api

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.

Elysia

typed HTTP surface

Drizzle

SQL-shaped data layer

BullMQ

background work

flowchart LR
  routes["routes.ts<br/>HTTP · TypeBox · no DB"]
  service["service.ts<br/>logic + Drizzle<br/>no Elysia"]
  types["types.ts<br/>shared interfaces"]
  routes --> service
  routes -.-> types
  service -.-> types

Each API feature splits into three files with one job each: routes.ts owns HTTP and TypeBox validation but never touches the database; service.ts owns business logic and Drizzle queries but never imports Elysia; types.ts holds the shared interfaces both read. Lint rules forbid the cross-imports that would blur the split.

A feature is three files with three jobs. Lint plugins forbid them from leaking into each other: a *.routes.ts that imports drizzle-orm fails the build, and a *.service.ts that imports Elysia’s t does too.

features

Per-feature folders

Adding product behavior means one folder, not scattered route/service/model edits.

lint

Routes, services, and types stay separate

The split survives refactors, new teammates, and agent-written code.

env

Frozen config at boot

process.env is read in one validator. Misconfigured deployments fail before serving traffic.

providers

Pluggable infra

Email, AI, cache, and queues swap through config; dev runs without vendor keys.

data

Drizzle ORM

TS-first models with SQL-shaped schema and real migration files.

contract

OpenAPI emits the UI boundary

The React app calls a generated client, so server changes become type errors instead of runtime surprises.

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

A feature folder always looks like (for a hypothetical posts resource):

Feature anatomy
src/api/posts/
  • posts.routes.tsHTTP surface
  • posts.service.tsBusiness logic + DB
  • posts.types.tsShapes shared between routes and service
  • posts.schemas.tsoptional TypeBox request/response

The shipped auth, users, accounts, billing, dashboard, admin, health, notifications modules are framework. widgets is the only example domain feature, kept as the reference for the account-scoped resource pattern (every read/write filters by accountId). Replace it with your own product domain. Add new resources with bun run new:resource <name>; the scaffolder writes the four-file anatomy and wires it into config/routes.ts so you can’t forget a step.

auth

Cookie sessions

Short-lived access JWT cookies plus DB-backed refresh sessions.

tenant

Account boundary

Accounts are the tenant boundary; users join via memberships with roles.

acl

Feature resolution

Server-authoritative CASL ability plus plan and feature gates.

billing

Stripe billing

Checkout, Customer Portal, raw-body webhooks, and DB-backed idempotency.

email

Provider abstraction

Pluggable provider, precompiled templates, queue-aware dispatch.

queues

Background work

BullMQ with QueueManager and inline fallback when queues are disabled.

audit

Append-only trail

Fire-and-forget append-only event log for user and system actions.

env

Boot guard

TypeBox shape plus hand-written invariants before the API listens.

The architecture is held in place by a family of custom ESLint plugins. bun run validate is the merge gate.

apps/api on GitHub. Start in src/api/ for the feature shape; src/config/ for the boot wiring.