Documentation

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 notes module from API modules, still returning the sample notes in sample-notes.ts.

Create the route

Create src/api/modules/notes/show.route.ts:

plugins/site-notes/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 the path a page passes to the fetcher.
  • request.params validates the URL segment. Path parameters arrive as strings, so z.coerce.number() turns "2" into 2, and .int().positive() refuses everything else.
  • c.req.valid("param") returns the parsed, typed value. Read inputs through c.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:

plugins/site-notes/src/api/modules/notes/notes.module.ts
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
RequestStatusBody
/notes/2200{ "id": 2, "title": "Office closed on Friday", ... }
/notes/9404empty
/notes/abc400{ "success": false, "error": { "name": "ZodError", ... } }

The 400 comes from validation, so your handler never ran.