serverstruct
Version:
Type safe and modular servers with H3
112 lines (91 loc) • 4.25 kB
Markdown
# Serverstruct OpenAPI Reference
Use this when the task specifically involves `serverstruct/openapi`.
## Installation
```sh
npm i zod zod-openapi
```
## Main Rules
- Call `useRouter(app)` once per app and register all routes through the returned router.
- Do not mix `app.*` and `router.*` on the same app after `useRouter(app)`.
- All `router.mount()` signatures will include OpenAPI paths when the mounted app already has a router attached. Prefer `router.mount(box, { "/prefix": controllerCtor })` when mounting multiple controllers because it also resolves them from the shared Box.
- Keep routes inline in controllers by default. Use standalone `route()` only when a route genuinely needs extraction.
- Prefer a class to organize route schemas, using static properties named after each route's `operationId` and created with `schemas()`. Add extra schema properties when a route needs them such as error responses or alternate response shapes.
- H3 and OpenAPI path syntax differ when working with paths directly. `useRouter()` and `route()` accept H3-style paths and convert them automatically, but `OpenApiPaths` expects OpenAPI-style paths such as `/{id}`.
## Route Definition
When adding or editing routes, define the contract from the schemas first, then fill the OpenAPI operation from that contract.
- Enrich Zod schemas with useful constraints, defaults, examples, and metadata before wiring them into the route.
- Put request and response schemas in the route's `schemas()` object, then reference them from `requestParams`, `requestBody`, and `responses`.
- Use `jsonRequest()` and `jsonResponse()` to specify JSON request and response entries from those schemas.
- Fill the OpenAPI operation completely: `operationId`, request params, request body, success responses, and expected error responses.
- Keep the handler aligned with the declared contract by using `ctx.params()`, `ctx.query()`, `ctx.body()`, and `ctx.reply()` or `ctx.validReply()`.
## Typical Pattern
```typescript
import { controller } from "serverstruct";
import {
jsonRequest,
jsonResponse,
schemas,
useRouter,
} from "serverstruct/openapi";
import { z } from "zod";
class PostSchemas {
static createPost = schemas({
body: z.object({ title: z.string() }),
response: z.object({ id: z.string(), title: z.string() }),
badRequest: z.object({ message: z.string() }),
});
}
const postsController = controller((app, box) => {
const router = useRouter(app);
const store = box.get(PostStore);
router.post(
"/",
{
operationId: "createPost",
requestBody: jsonRequest(PostSchemas.createPost.body),
responses: {
201: jsonResponse(PostSchemas.createPost.response, {
description: "Post created",
}),
400: jsonResponse(PostSchemas.createPost.badRequest, {
description: "Bad request",
}),
},
},
async (event, ctx) => {
const body = await ctx.body(event);
return ctx.reply(event, 201, await store.create(body));
},
);
});
```
## Typed Helpers
- `ctx.params(event)` validates and parses path params
- `ctx.query(event)` validates and parses query params
- `ctx.body(event)` validates and parses JSON body
- `ctx.reply(event, status, body)` sets status and returns typed response data
- `ctx.validReply(...)` also validates the response at runtime
Validation failures become `HTTPError` 400 responses.
Common pitfall: do not use response schemas with output transforms when returning through `ctx.reply()`. `reply()` is type-only for responses and does not run runtime response parsing, so transformed output shapes are only enforced when using `ctx.validReply()`.
## Extracted Routes
Use `route()` when inline controller routes are not enough.
```typescript
const getPost = route({
method: "get",
path: "/posts/:id",
operation: {
operationId: "getPost",
requestParams: { path: z.object({ id: z.string() }) },
responses: {
200: jsonResponse(postSchema, { description: "Success" }),
},
},
setup(box) {
const db = box.get(Database);
return async (event, ctx) => {
const { id } = await ctx.params(event);
return ctx.reply(event, 200, await db.findPost(id));
};
},
});
```