Defining New Routes
Step-by-step guide on how to create a new API endpoint with validation and OpenAPI metadata.
This guide walks you through adding a new API route in MuseMVP, following the existing patterns for validation, middleware, and OpenAPI documentation.
Step-by-Step Flow
Write failing tests first; update schema/queries in packages/db/src/ with db first, then bind them in app database wrappers. Orchestration belongs in src/modules/<feature>/lib/.
Create a new Hono router or extend an existing one in src/backend/api/routes/.
Register the router in src/backend/api/app.ts.
Add API client hooks in src/backend/api-client/ for frontend consumption.
1. Define the Route Handler
Create a new router file or add to an existing one. Use Zod for validation and describeRoute for OpenAPI metadata.
/** New file: src/backend/api/routes/example/router.ts */
import { Hono } from "hono";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod";
import { authMiddleware } from "@/backend/api/middleware/auth";
const querySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(10),
offset: z.coerce.number().int().min(0).default(0),
});
const responseSchema = z.object({
items: z.array(z.object({ id: z.string(), name: z.string() })),
total: z.number(),
});
export const exampleRouter = new Hono()
.basePath("/example")
.use(authMiddleware)
.get(
"/",
validator("query", querySchema),
describeRoute({
tags: ["Example"],
summary: "List items",
responses: {
200: {
description: "Items returned",
content: {
"application/json": {
schema: resolver(responseSchema),
},
},
},
},
}),
async (c) => {
const { limit, offset } = c.req.valid("query");
const user = c.get("user");
/** Call the owning feature service using validated input. */
return c.json({ items: [], total: 0 });
},
);2. Register the Router
Add the new router to the main app in src/backend/api/app.ts:
import { exampleRouter } from "./routes/example/router";
const appRouter = app
.route("/", authRouter)
.route("/", exampleRouter);
/** Preserve the existing route registrations. */3. Validation Patterns
| Location | Use Case | Example |
|---|---|---|
validator("query", schema) | Query params | Pagination, search |
validator("json", schema) | JSON body | Create/update payloads |
validator("form", schema) | Form data | Contact form, file upload |
validator("param", schema) | Path params | /conversations/:id |
4. Middleware Options
| Middleware | When to Use |
|---|---|
authMiddleware | Requires logged-in user; sets c.get("user") and c.get("session") |
adminMiddleware | Requires admin role; use for admin-only endpoints |
localeMiddleware | Sets c.get("locale") from request/cookie for i18n |
5. API Client Hook
Create a TanStack Query hook that calls your new endpoint:
/** New file: src/backend/api-client/example/use-example.ts */
import { useQuery } from "@tanstack/react-query";
import { apiClient } from "@/backend/api-client/api-client";
export function useExample(params?: { limit?: number; offset?: number }) {
return useQuery({
queryKey: ["example", params],
queryFn: async () => {
const query = {
...(params?.limit !== undefined ? { limit: String(params.limit) } : {}),
...(params?.offset !== undefined ? { offset: String(params.offset) } : {}),
};
const res = await apiClient.example.$get({ query });
if (!res.ok) throw new Error("Fetch failed");
return res.json();
},
});
}Quick Reference
- Admin route:
admin/users.ts—adminMiddleware, query validation,describeRoute - Public route:
contact/router.ts—localeMiddleware, form validation - Auth route:
assets/router.ts—authMiddleware, query + json validation,describeRoute
Add unit/API contracts for unauthenticated access, permissions, invalid input, and errors. Update tests/e2e/SCENARIOS.md for new browser journeys, synchronize app/API copies, and run pnpm apps:parity.