API routes
Add a route with a validated path parameter to the site-notes VitNode plugin, answer 404 for a missing note, and see how invalid input is refused.
Each route declares its inputs and outputs with Zod, and VitNode validates the request before your handler runs. In this lesson you add GET /api/site-notes/notes/{id}, which returns one note, refuses an id that is not a positive number, and answers 404 when no note has that id.
Before you begin
- The
notesmodule from API modules, still returning the sample notes insample-notes.ts.
Create the route
Create src/api/modules/notes/show.route.ts:
import { z } from "@hono/zod-openapi";
import { buildRoute } from "@/api/builders";
import { zodNote } from "./notes.schema";
import { sampleNotes } from "./sample-notes";
export const showNoteRoute = buildRoute({
route: {
method: "get",
path: "/{id}",
request: {
params: z.object({ id: z.coerce.number().int().positive() }),
},
responses: {
200: {
content: { "application/json": { schema: zodNote } },
description: "The note.",
},
404: { description: "No note has this id." },
},
},
handler: c => {
const { id } = c.req.valid("param");
const note = sampleNotes.find(item => item.id === id);
if (!note) return c.body(null, 404);
return c.json(note, 200);
},
});path: "/{id}"uses OpenAPI's parameter syntax. It is also thepatha page passes to the fetcher.request.paramsvalidates the URL segment. Path parameters arrive as strings, soz.coerce.number()turns"2"into2, and.int().positive()refuses everything else.c.req.valid("param")returns the parsed, typed value. Read inputs throughc.req.valid(), never from the raw request.- List every status the handler returns in
responses. That is what lets the fetcher type both outcomes for the caller.
Body and query parameters work the same way, through request.body and request.query; the Permissions lesson adds a validated body.
Add it to the module
Open src/api/modules/notes/notes.module.ts and add the route after the list route. Keep listNotesRoute:
import { buildModule } from "@/api/builders";
import { listNotesRoute } from "./list.route";
import { showNoteRoute } from "./show.route";
export const notesModule = buildModule({
name: "notes",
routes: [listNotesRoute],
routes: [listNotesRoute, showNoteRoute],
});Check the result
Ask for an existing note, a missing one and an invalid id:
curl -i http://localhost:3000/api/site-notes/notes/2
curl -i http://localhost:3000/api/site-notes/notes/9
curl -i http://localhost:3000/api/site-notes/notes/abc| Request | Status | Body |
|---|---|---|
/notes/2 | 200 | { "id": 2, "title": "Office closed on Friday", ... } |
/notes/9 | 404 | empty |
/notes/abc | 400 | { "success": false, "error": { "name": "ZodError", ... } } |
The 400 comes from validation, so your handler never ran.