API modules
Add a notes module to the site-notes VitNode plugin, register it in config.api.ts, and call its typed endpoint through the host API.
An API module groups a plugin's Hono routes under one URL segment. In this lesson you add a notes module to site-notes whose endpoint returns two sample notes. A later lesson swaps the sample data for a database table.
Before you begin
- The
site-notesplugin from Create plugin, withpnpm devrunning at the workspace root.devrebuilds the plugin every time you save.
Describe a note
Create src/api/modules/notes/notes.schema.ts. The schema is the response shape, and OpenAPI documents it too:
import { z } from "@hono/zod-openapi";
export const zodNote = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
});Then create the sample data the endpoint returns for now:
/** Placeholder notes until the Database lesson stores real ones. */
export const sampleNotes = [
{ id: 1, title: "Welcome to site notes", body: "Short updates for everyone on the site." },
{ id: 2, title: "Office closed on Friday", body: "We are back on Monday at 9:00." },
];Create the list route
Create src/api/modules/notes/list.route.ts. buildRoute comes from the plugin's own src/api/builders.ts, which the generator wrote, so the route is tied to site-notes without naming it:
import { z } from "@hono/zod-openapi";
import { buildRoute } from "@/api/builders";
import { zodNote } from "./notes.schema";
import { sampleNotes } from "./sample-notes";
export const listNotesRoute = buildRoute({
route: {
method: "get",
path: "/",
responses: {
200: {
content: { "application/json": { schema: z.array(zodNote) } },
description: "Every note, newest first.",
},
},
},
handler: c => c.json(sampleNotes, 200),
});The responses schema is a contract. c.json() only accepts a body and status that match it.
Group it in a module
Create src/api/modules/notes/notes.module.ts. The module's name is the URL segment:
import { buildModule } from "@/api/builders";
import { listNotesRoute } from "./list.route";
export const notesModule = buildModule({
name: "notes",
routes: [listNotesRoute],
});Register the module
Open src/config.api.ts and add notesModule next to the generated helloModule. This is an excerpt; keep the rest of the file, including the VitNodeApiPlugin type at the bottom:
import { helloModule } from "./api/modules/hello/hello.module";
import { notesModule } from "./api/modules/notes/notes.module";
export const siteNotesApiPlugin = () =>
buildApiPlugin({
manifest,
modules: [helloModule],
modules: [helloModule, notesModule],
});The generator already added a siteNotesApiPlugin() call to the app's vitnode.api.config.ts. Every module you list here is served under /api/site-notes/, so nothing changes in the app.
Check the result
curl http://localhost:3000/api/site-notes/notes[
{ "id": 1, "title": "Welcome to site notes", "body": "Short updates for everyone on the site." },
{ "id": 2, "title": "Office closed on Friday", "body": "We are back on Monday at 9:00." }
]The endpoint also appears in the app's API reference; see Swagger.
How the fetcher knows your routes
Pages call plugin APIs through the fetcher, which infers module, path, the arguments and the response from a registry of plugin types. You never edit that registry by hand:
- In the plugin, every
vitnode buildandvitnode dev(both run bypnpm dev) writestypes/api-registry.gen.d.ts. It registers only this plugin, as a type import ofVitNodeApiPluginfromsrc/config.api.ts, so the plugin's own pages type-check before any app installs it. The plugin'stsconfig.jsonincludestypes/,.npmignorekeeps the folder out of the published package, and you commit it likesrc/manifest.gen.ts. - In the app,
vitnode devandvitnode buildwritesrc/api-registry.gen.tsfrom the plugins the app configured. That file, not the plugin's, decides what the app's pages can call.
The VitNodeApiPlugin type at the bottom of config.api.ts is the link between the two. It keeps the plugin id, module paths and route definitions and drops the Hono app, database and listener code, so a type-only import never pulls server code into a browser build. The generator writes it; keep it when you edit the file.