@md-oss/api-types
v0.10.0
Published
Type-safe API contracts, client factory, and handler helpers built on Zod
Readme
@md-oss/api-types
Type-safe API contracts and helpers for building clients and route handlers around shared @md-oss/common errors and Zod-validated inputs/outputs.
Features
- Route registries with Zod schemas for params, query, body, and optional response validation
- Typed API client factory (
createApiClient) that strips unsafe headers and returns full HTTP response metadata (statusCode,headers, rawResponse, etc.) - Generic controller/route handler builders (
createGenericController,createGenericRouteHandler) with pluggable auth/context/permission strategies - Response helpers (
sendTypedResponse) and request parsers (parseRequestParameters) that serialize/validate against your route contract - Utilities for prefixing routes, parsing/stripping proxy headers, and fine-grained debug namespaces (
md-oss:api-types:*)
Installation
pnpm add @md-oss/api-typesDefine a typed route registry
import { z } from 'zod';
import type { RouteRegistry } from '@md-oss/api-types';
const routes = {
'/users/:id': {
params: z.object({ id: z.string() }),
endpoints: {
GET: {
response: { id: '123', email: '[email protected]' },
permissions: null,
},
},
},
'/posts': {
endpoints: {
POST: {
body: z.object({ title: z.string(), body: z.string() }),
response: { id: 'post-id' },
permissions: { role: 'editor' },
},
},
},
} satisfies RouteRegistry;Create a typed client
import { createApiClient } from '@md-oss/api-types';
const client = createApiClient(routes, {
baseUrl: 'https://api.example.com',
});
const result = await client.request('/users/:id', {
method: 'GET',
params: { id: '123' },
});
if (!result.ok) {
// HTTP error response with metadata
}
const user = result.data;Opt-in response validation with Zod
When an endpoint declares response as a Zod schema, response typing and runtime validation are both enabled automatically.
You can also declare responses as a status-code map of schemas.
response and responses are mutually exclusive. Use exactly one.
import { z } from 'zod';
import type { RouteRegistry } from '@md-oss/api-types';
const routes = {
'/users/:id': {
params: z.object({ id: z.string() }),
endpoints: {
GET: {
response: z.object({ id: z.string(), email: z.email() }),
permissions: null,
},
},
},
} satisfies RouteRegistry;- Server:
sendTypedResponsevalidates the outgoing body when a response schema is present. - Client:
createApiClient(...).request(...)validates successful responses when a response schema is present. - Non-Zod
responsevalues continue to work as before (type-only behavior, no runtime validation).
Status-code response schemas (responses)
const routes = {
'/users/:id': {
params: z.object({ id: z.string() }),
endpoints: {
GET: {
responses: {
200: z.object({ id: z.string(), email: z.email() }),
304: z.null(),
default: apiErrorResponseSchema // <- Used if status code not included in mapping
},
permissions: null,
},
},
},
} satisfies RouteRegistry;responses[statusCode]is used for runtime validation when present.- If
responsesis used and a status code has no schema, no runtime response validation is applied for that status.
Build controllers with typed context
import {
createGenericController,
sendTypedResponse,
type ContextProvider,
} from '@md-oss/api-types';
const authStrategy = {
async resolveAuthentication(req, res, endpoint) {
// return { session: { userId: 'u1' } } or { session: null }
return { session: null };
},
};
const contextStrategy = {
async buildContext(session, endpoint, parsed, injected, req, res, requestId) {
return {
...parsed,
session,
endpoint,
ctx: { requestId },
cps: async () => true,
} satisfies ContextProvider<typeof routes, any, '/users/:id', 'GET', null>;
},
};
const getUser = createGenericController(
routes,
'/users/:id',
'GET',
{ authStrategy, contextStrategy }
)((context, respond) => {
respond({
path: '/users/:id',
method: 'GET',
data: { id: context.params.id, email: '[email protected]' },
});
});
// use getUser as an Express/Next/fastify style handlercreateGenericRouteHandler powers .withContext(...) so you can inject pre-built context when wiring routes.
Validate requests and respond consistently
parseRequestParametersvalidates params/query/body against Zod schemas and builds a typed context payload.sendTypedResponsereturns exactly thedatayou pass in.- Signed access errors can be converted to
HTTPErrorviaparseSignedAccessError.
Model envelope-style APIs with schemas
For APIs that use envelope response bodies, define a re-usable zod schema:
import { z } from 'zod/v4';
import { extendDefaultHttpResponseEnvelope } from '@md-oss/common/http/schemas';
const apiResponseEnvelope = <D extends z.ZodTypeAny>(data: D) =>
extendDefaultHttpResponseEnvelope(data, {
rid: z.uuid(),
});
const routes = {
'/': {
endpoints: {
GET: {
permissions: { requireAuthentication: false },
responses: {
200: apiResponseEnvelope(apiInfoResponseDataSchema),
default: httpErrorResponseSchema,
},
},
},
},
'/health': {
endpoints: {
GET: {
permissions: { requireAuthentication: false },
responses: {
200: apiResponseEnvelope(healthResponseDataSchema),
default: httpErrorResponseSchema,
},
},
},
},
} satisfies RouteRegistry;This keeps the core transport behavior simple while still supporting arbitrary envelope shapes (including fields like rid) through your own schemas.
sendTypedResponse utilities
You can wrap sendTypedResponse to set defaults and/or extend behavior:
import {
createGenericController,
extendSendTypedResponse,
withSendTypedResponseDefaults,
} from '@md-oss/api-types';
const sendWithDefaults = withSendTypedResponseDefaults(
{
headers: {
'x-api-version': '2026-05-05',
},
},
);
const sendWithDefaultsAndAudit = extendSendTypedResponse(
({ options, res, next }) => {
res.setHeader('x-request-id', options.path);
next(res, options);
},
sendWithDefaults
);
// Respond directly anywhere
sendWithDefaultsAndAudit(res, {
path: TPath;
method: TMethod;
data: API[TPath]['endpoints'][TMethod]['response'];
status?: number;
headers?: Record<string, string>;
responseSchemas?: ResponseSchemas;
});
// Or use in a route-handler
const getUserController = createGenericController(
routes,
'/users/:id',
'GET',
{ authStrategy, contextStrategy, permissionStrategy, sendWithDefaultsAndAudit }
)((context, respond) => {
respond({
data: { id: context.params.id, email: '[email protected]' },
});
});Debugging
Enable scoped debugging with DEBUG=md-oss:api-types* to trace parameter parsing, performance timings, and controller responses. Namespaces include md-oss:api-types:route, :performance, and :errors.
Exports
Key exports from the package entrypoint:
- Client:
createApiClient,parseHeaders,stripProxyAndWebsocketHeaders,ApiClient - Server:
createGenericController,createGenericRouteHandler,sendTypedResponse,parseRequestParameters - Types:
RouteRegistry,EndpointDefinition,InferApi,RouteHandler,ControllerFunction,RequestOptions,ExtractResolvedContext,PrefixRoutes,RouteKeys,MethodKeys
See the source in src/ for strategy interfaces (auth, context, permission tracking) and additional helpers.
