Skip to main content

Overview

Every API route in folksbase follows the same pattern: a Hono route handler with OpenAPI documentation via hono-openapi, request validation via Zod, and strict adherence to the layered architecture (Routes → Services → Repositories → Drizzle). This guide walks through adding a new route from scratch.

The Layered Architecture

Before writing any route code, understand the layers: Hard rules:
  • Routes never import from @folksbase/db
  • Services never construct HTTP responses
  • Repositories never contain business logic

Step 1: Define Your Zod Schemas

Start with the request and response schemas. If they’re reusable, add them to apps/api/src/lib/openapi-schemas.ts. Otherwise, define them in the route file.

Step 2: Create the Route File

Create a new file in apps/api/src/routes/. Import the OpenAPI helpers and auth middleware:

Step 3: Add Route Handlers with OpenAPI Docs

Every route must use describeRoute() for response documentation and zValidator() for request validation:

Key Patterns

  • Path params use zValidator("param", schema) — always enforce UUID format:
  • Query params use zValidator("query", schema)
  • JSON body uses zValidator("json", schema)
  • Access validated data via c.req.valid("query"), c.req.valid("json"), or c.req.valid("param")

Step 4: Register the Route

Add your route to apps/api/src/index.ts:

Step 5: Error Handling

Don’t add manual try/catch blocks in route handlers. Let errors propagate to the global errorHandler middleware, which handles ZodError and auth errors automatically and returns consistent { code, message } responses. The only exception is when you need to return a specific 404:

Step 6: Verify

After adding your route:
  1. Start the dev server: pnpm --filter @folksbase/api dev
  2. Open http://localhost:3001/api/docs — your new route should appear in the Scalar UI
  3. Check the OpenAPI spec at http://localhost:3001/api/openapi.json
  4. Write unit tests in src/routes/__tests__/ following the existing patterns

Checklist

Before submitting your PR, verify:
  • Route uses describeRoute() with tags, summary, and response schemas
  • Request validation uses zValidator() (not manual .parse())
  • Route only calls services — no direct DB imports
  • Path params enforce UUID format where applicable
  • No manual try/catch — errors propagate to global handler
  • Unit tests use valid UUIDs for path parameters
  • Route is registered in src/index.ts