Module Kit

Module Kit server toolkit

The helpers a module's server routes use for errors, parameters, pagination, responses, CORS and tests.

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.

HelperPurpose
apiError(status, code, message, data?)Any status and code.
badRequest, forbidden, notFound, conflict, notConfiguredOne 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.

SchemaAccepts
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.

HelperReturns
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.

HelperPurpose
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:

server/middleware/api-cors.ts
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 ''
})
OptionDefaultHeader
originsrequiredAccess-Control-Allow-Origin: the request's Origin, when listed exactly. * never matches.
methodsrequiredAccess-Control-Allow-Methods, as an array or a string.
allowHeadersauthorization, content-typeAccess-Control-Allow-Headers.
exposeHeadersnoneAccess-Control-Expose-Headers, such as www-authenticate.
maxAge600Access-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 OPTIONS request from a listed origin with 204 and returns true, so the middleware ends it. Any other request returns false and 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.

HelperWhat 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

Copyright © 2026