> ## Documentation Index
> Fetch the complete documentation index at: https://docs.folksbase.joselito.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Adding API Routes

> How to add new Hono routes with OpenAPI documentation, validation, and proper layering.

## 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:

```mermaid theme={"dark"}
graph TD
    A[Route Handler] -->|calls| B[Service Layer]
    B -->|calls| C[Repository Layer]
    C -->|queries| D[Drizzle + Neon]
```

| Layer      | Responsibility                                                    | Location                |
| ---------- | ----------------------------------------------------------------- | ----------------------- |
| Route      | HTTP concerns only — parse request, call service, return response | `src/routes/*.ts`       |
| Service    | Business logic, orchestration, validation                         | `src/services/*.ts`     |
| Repository | SQL queries only, no business logic                               | `src/repositories/*.ts` |

**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.

```typescript theme={"dark"}
import { z } from "zod";

const createBodySchema = z.object({
  name: z.string().min(1).max(100),
  color: z.string().max(20).optional(),
});

const updateBodySchema = createBodySchema.partial();
```

## Step 2: Create the Route File

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

```typescript theme={"dark"}
import { errorSchema } from "@/lib/openapi-schemas.js";
import { type Variables, auth } from "@/middleware/auth.js";
import { Hono } from "hono";
import { describeRoute, resolver, validator as zValidator } from "hono-openapi";
import { z } from "zod";
```

## Step 3: Add Route Handlers with OpenAPI Docs

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

```typescript theme={"dark"}
export const widgetsRoute = new Hono<{ Variables: Variables }>()
  .use("*", auth)

  .get(
    "/",
    describeRoute({
      tags: ["Widgets"],
      summary: "List widgets",
      description: "Returns all widgets for the workspace.",
      responses: {
        200: {
          description: "Widget list",
          content: {
            "application/json": {
              schema: resolver(z.array(widgetSchema)),
            },
          },
        },
      },
    }),
    async (c) => {
      const { workspaceId } = c.get("user");
      const widgets = await widgetsService.findAll(workspaceId);
      return c.json(widgets);
    },
  )

  .post(
    "/",
    describeRoute({
      tags: ["Widgets"],
      summary: "Create widget",
      responses: {
        201: {
          description: "Widget created",
          content: {
            "application/json": { schema: resolver(widgetSchema) },
          },
        },
        400: {
          description: "Validation error",
          content: {
            "application/json": { schema: resolver(errorSchema) },
          },
        },
      },
    }),
    zValidator("json", createBodySchema),
    async (c) => {
      const { workspaceId } = c.get("user");
      const body = c.req.valid("json");
      const widget = await widgetsService.create(workspaceId, body);
      return c.json(widget, 201);
    },
  );
```

### Key Patterns

* **Path params** use `zValidator("param", schema)` — always enforce UUID format:
  ```typescript theme={"dark"}
  zValidator("param", z.object({ id: z.string().uuid() }))
  ```
* **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`:

```typescript theme={"dark"}
import { widgetsRoute } from "@/routes/widgets.js";

// Inside the app setup
app.route("/api/widgets", widgetsRoute);
```

## 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:

```typescript theme={"dark"}
const widget = await widgetsService.findById(workspaceId, id);
if (!widget) {
  return c.json({ code: "NOT_FOUND", message: "Widget not found" }, 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](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](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`
