API patterns
Voyzu exposes package APIs through one Next.js catch-all route:
apps/web/app/api/[[...voyzuApiPath]]/route.tsPackages do not add files beneath apps/web/app/api. Each module declares its routes in apiDefinitions, and composition adds installed packages to the runtime API registry.
Define routes in module.ts
Each API definition must provide an HTTP method, a path, and an asynchronous handler. Keep the handler implementation in the module's server boundary.
// packages/@acme/warehousing/modules/stock/module.ts
import type { VoyzuPackageModuleDefinition } from "@voyzu/types/framework";
import { handleCreate, handleGet, handleList } from "./server";
export const stockModule = {
pageRoutes: {},
apiDefinitions: {
list: {
method: "GET",
path: "/stock",
handler: (request: any) => handleList(request),
},
create: {
method: "POST",
path: "/stock",
handler: (request: any) => handleCreate(request),
},
get: {
method: "GET",
path: "/stock/[code]",
handler: (request: any, context: any) =>
handleGet(request, context),
},
},
} as const satisfies VoyzuPackageModuleDefinition;An API-only module may use an empty pageRoutes object.
At runtime the route is prefixed with /api, so the example above exposes GET /api/stock, POST /api/stock, and GET /api/stock/{code}.
Implement the standard API methods
Unless a module has a particular reason to expose a different contract, it should implement the following 17 API methods. Using this standard gives Voyzu modules predictable CRUD, search, batch, and lifecycle operations.
List ice creams
GET /ice-creams
Create an ice cream
POST /ice-creams
Filter ice creams
POST /ice-creams/filter
Search ice creams
GET /ice-creams/search?q=...
Batch create ice creams
POST /ice-creams/batch/create
Batch get ice creams
POST /ice-creams/batch/get
Batch update ice creams
PUT /ice-creams/batch/update
Batch patch ice creams
PATCH /ice-creams/batch/patch
Batch delete ice creams
POST /ice-creams/batch/delete
Batch activate ice creams
POST /ice-creams/batch/activate
Batch deactivate ice creams
POST /ice-creams/batch/deactivate
Activate an ice cream
POST /ice-creams/[code]/activate
Deactivate an ice cream
POST /ice-creams/[code]/deactivate
Get an ice cream
GET /ice-creams/[code]
Update an ice cream
PUT /ice-creams/[code]
Patch an ice cream
PATCH /ice-creams/[code]
Delete an ice cream
DELETE /ice-creams/[code]
The activate, deactivate, and batch command paths are intentional parts of the Voyzu API convention. Do not invent alternative names or HTTP methods for these standard operations. A module should omit an operation when its domain rules make that operation inappropriate. For example, the Voyzu Audit module is read-only: audit events are immutable records and cannot be created, updated, activated, deactivated, or deleted through the Audit API. It therefore exposes only the read operations that make sense for audit events rather than implementing all 17 methods. Document the reason for any such deviation in the module.
The combination of method and path must be unique across all composed modules. Use the package's domain vocabulary in paths to avoid collisions with other packages.
Document every API operation
Every API definition must include an apiDoc object. It is the source for the Voyzu API Reference and the combined OpenAPI document. Document the operation's purpose, every input, its successful response, and every error response callers can receive.
The following Ice Creams example is deliberately exhaustive: it shows every supported API documentation field in one definition. A real operation should include only the path parameters, query-string parameters, cookies, body, and responses that form part of its contract.
Use dtoRef("DtoName") for one DTO and arrayOf(dtoRef("DtoName")) for an array response or request. The generator resolves those references to exported TypeScript DTOs and derives JSON schemas and representative examples from their types.
Comment DTO fields
Add a concise JSDoc comment immediately above every DTO field. These comments become field descriptions in the generated API Reference and OpenAPI schemas. Describe meaning or constraints that the TypeScript type alone cannot express.
voyzu:compose reads the registered modules of every active package, extracts their apiDefinitions, resolves referenced DTOs, and writes package-grouped operation and DTO documents together with the combined OpenAPI document. The API Reference UI reads those generated files; it does not inspect handlers at runtime.
Use dynamic path parameters
Dynamic segments use Next.js bracket syntax in module.ts:
The router resolves matched values through context.params:
Validate all path and query-string values before passing them to a service.
Keep handlers thin
HTTP handlers must:
parse path, query-string, cookie, and body input;
validate the request contract;
call a server service;
translate the result into a response DTO; and
map known errors to appropriate HTTP status codes.
Handlers must not contain persistence queries or duplicate business rules. Services own business operations and repositories own SQL.
Return standard error responses
Handlers must translate known failures to the shared Voyzu error-response DTOs. Every apiDoc.responses object must document each error the operation can return as well as its successful response.
400
The path, query string, or request body is malformed or fails validation.
InputValidationErrorResponseDto
401
The caller is not authenticated.
UnauthorizedErrorResponseDto
403
The authenticated caller lacks permission.
Runtime authorization response
404
The requested entity does not exist.
EntityNotFoundErrorResponseDto
409
The request conflicts with an existing value or state, such as a duplicate code.
ConflictErrorResponseDto
422
The request is valid but a business rule blocks the operation.
BusinessRuleErrorResponseDto
500
An unexpected server failure occurs.
InternalServerErrorResponseDto
Use the matching shared error classes in services and translate them at the HTTP boundary. Authentication and authorization may be handled by the runtime before the module handler runs. Unexpected errors must be logged server-side and returned as the standard 500 response; never expose raw database errors, stack traces, or other implementation details.
Use 200 for successful reads and updates with a response body, 201 for successful creates, and 204 for successful operations that return no body.
Keep DTOs at the package boundary
Request and response DTOs belong to the package that owns the API. Export DTOs that other packages are allowed to consume through an explicit package.json export. Use @voyzu/types for framework-wide contracts such as filtering, standard errors, auditing, and package definitions.
Do not use database row types as API DTOs. Map rows to explicit response objects so that internal schema changes do not silently change the public API.
Compose API changes
Run npm run voyzu:compose after adding or changing an API definition or DTO. Composition reads each active package's voyzu.package.ts, rebuilds the runtime API registry, generates the package-grouped API documentation and combined OpenAPI document, and clears the Next.js cache.
Do not edit generated registry or API documentation files. Restart the web server after voyzu:compose completes so the application loads the regenerated routes and documentation.
Last updated