Module Kit server toolkit
This page lists the helpers your module's route handlers use, so every module's API answers the same way. It is for module authors, and the rules for using them are in HTTP routes. Set up the module first: Overview.
Import the helpers from @frigear-nu/module-kit/server. They are not auto-imported, so they never clash with h3's own
helpers.
Registering routes
addModuleRoutes registers all of a module's routes from one table in setup(). It comes from
@frigear-nu/module-kit/utils, not /server.
function addModuleRoutes(
resolver: Resolver,
routes: Array<{ method: 'get' | 'post' | 'put' | 'patch' | 'delete', path: string, file: string }>,
options: { base: string },
): void
file is relative to ./runtime/server/routes/. path is joined onto base, '' is base itself, and a path that
starts with / ignores base. Registering routes has a full
example.
Errors
Each helper returns an h3 error to throw, with the message in statusMessage and a snake_case code in data.code.
Errors has the status and code of each, and examples.
| Helper | Purpose |
|---|---|
apiError(status, code, message, data?) | Any status and code. |
badRequest, forbidden, notFound, conflict, notConfigured | One status each, each with a default message. |
validationError(issues, message?) | A 400 with one issue per field in data.issues. |
Params
Two zod schemas validate ids the same way in every module. Pass them to h3's getValidatedRouterParams.
| Schema | Accepts |
|---|---|
idParams(name = 'id') | A non-empty string. |
intIdParams(name = 'id') | A positive integer, coerced from the path. |
const { id } = await getValidatedRouterParams(event, idParams().parse)
const { tokenId } = await getValidatedRouterParams(event, idParams('tokenId').parse)
const { id: userId } = await getValidatedRouterParams(event, intIdParams().parse)
Pagination
These helpers turn a list query into a page, in the query keys that useTableQuery sends.
Pagination has a full list handler.
| Helper | Returns |
|---|---|
pageQuery({ sort, defaultSort?, defaultOrder?, defaultPageSize?, maxPageSize?, filters? }) | The zod schema of a list query. |
pageRange(query) | { limit, offset } for the query's page. |
paginate({ items, total, query }) | Page<T>: { items, total, page, pageSize }. |
orderByQuery(columns, query, { tiebreaker }) | An orderBy from query.sort and query.order. |
likeContains(column, value) | A case-insensitive "contains" search. |
likeEscaped(column, pattern), escapeLike(value) | The search behind it, with % and _ escaped. |
pageQuery accepts page (from 1), pageSize (default 20, at most 100), q, sort, order and your filters
keys. sort defaults to defaultSort or the first name, and order to desc.
orderByQuery always sorts by the tiebreaker last, descending, so pages stay stable. Pass the id.
likeContains works on both dialects. A % or _ in the search is matched literally.
Responses and helpers
Responses explains when to use each response helper.
| Helper | Purpose |
|---|---|
created(event, dto), noContent(event), isoDate(value) | 201 with a DTO, 204 with no body, and dates as ISO strings. |
uniqueCopySlug(base, exists) | The first free <base>-copy, <base>-copy-2, and so on. |
resolveSameOriginUrl(event, url, { fallback?, name? }) | A return URL on the request's origin, fallback when empty, 400 for another origin. |
sameOriginUrl(input, origin) | The same check without an event. Returns the normalised URL, or null. |
applyBearerCors(event, options) | CORS for callers with an access token on other origins. See Bearer CORS. |
matchesPathPrefix(pathname, prefixes) | Whether a path is under a prefix. /api/ matches /api and /api/x, not /apis. |
Bearer CORS
A browser app on another origin needs CORS headers to call your API with an access token. Examples are the
capacitor://localhost webview of a mobile app, or a single-page app. It must never get cookies across origins.
applyBearerCors adds exactly those headers, from a middleware, for the paths you choose:
import { applyBearerCors, matchesPathPrefix } from '@frigear-nu/module-kit/server'
export default defineEventHandler((event) => {
if (!matchesPathPrefix(getRequestURL(event).pathname, ['/api/'])) return
const preflight = applyBearerCors(event, {
origins: ['capacitor://localhost', 'https://localhost'],
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['authorization', 'content-type', 'accept'],
exposeHeaders: ['www-authenticate'],
})
if (preflight) return ''
})
| Option | Default | Header |
|---|---|---|
origins | required | Access-Control-Allow-Origin: the request's Origin, when listed exactly. * never matches. |
methods | required | Access-Control-Allow-Methods, as an array or a string. |
allowHeaders | authorization, content-type | Access-Control-Allow-Headers. |
exposeHeaders | none | Access-Control-Expose-Headers, such as www-authenticate. |
maxAge | 600 | Access-Control-Max-Age, in seconds. |
- It always adds
Vary: Origin, so caches keep one response per origin. - It sets the CORS headers only for a listed origin.
- It never sends
Access-Control-Allow-Credentials. - It answers an
OPTIONSrequest from a listed origin with204and returnstrue, so the middleware ends it. Any other request returnsfalseand continues.
The cors.paths option of OAuth Server and
OAuth Resource both use it.
Testing routes
@frigear-nu/module-kit/testing has two helpers for end-to-end tests against a
test fixture started with setup() from @nuxt/test-utils/e2e.
| Helper | What it does |
|---|---|
apiClient({ cookie?, bearer? }) | Acts as one caller. |
.json<T>(method, path, body?) | Resolves with the parsed body of a 2xx response, or null for 204. Rejects otherwise. |
.fetch(method, path, body?) | Resolves with the raw Response, whatever its status. |
expectApiError(promise, status, code?) | Asserts the request failed with status, and data.code when given. Resolves with the error body. |
Testing routes has an example.
Next steps
- HTTP routes – the URL, handler order and response rules these helpers serve.
- Module Kit database helpers – give your routes a table to read.
- Testing – boot a test fixture for your route tests.