Documentation

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-notes plugin from Create plugin, with pnpm dev running at the workspace root. dev rebuilds 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:

plugins/site-notes/src/api/modules/notes/notes.schema.ts
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:

plugins/site-notes/src/api/modules/notes/sample-notes.ts
/** 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:

plugins/site-notes/src/api/modules/notes/list.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 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:

plugins/site-notes/src/api/modules/notes/notes.module.ts
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:

plugins/site-notes/src/config.api.ts
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 build and vitnode dev (both run by pnpm dev) writes types/api-registry.gen.d.ts. It registers only this plugin, as a type import of VitNodeApiPlugin from src/config.api.ts, so the plugin's own pages type-check before any app installs it. The plugin's tsconfig.json includes types/, .npmignore keeps the folder out of the published package, and you commit it like src/manifest.gen.ts.
  • In the app, vitnode dev and vitnode build write src/api-registry.gen.ts from 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.