Express Zod API 
Start your API server with I/O schema validation and custom middlewares in minutes.
Technologies
Concept
The API always operates object schemas for input and output.
For GET method it provides request.query
for middlewares and handler as input
and all properties are string
.
Input schema may also have transformations for incoming GET params (see the example below).
For POST, PUT and PATCH the input
is request.body
(parsed JSON) so properties may have different types.
The handler's argument options
comes from the returns of the middlewares, which can also supplement and transform the input
argument.
All inputs and outputs are validated against their object schemas and ResultHandler
handles the output or possible validation or thrown errors.

Installation
yarn add express-zod-api
# or
npm install express-zod-api
Add the following options to your tsconfig.json
file in order to make it work as expected:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true
}
}
Basic usage
See full example here.
Set up config
import {ConfigType} from 'express-zod-api';
const config: ConfigType = {
server: {
listen: 8090,
},
cors: true,
logger: {
level: 'debug',
color: true
}
};
See config-type.d.ts
for all available options.
Create an endpoints factory
import {EndpointsFactory} from 'express-zod-api';
const endpointsFactory = new EndpointsFactory();
You can also instantly add middlewares to it using .addMiddleware()
method.
Create your first endpoint
import {z} from 'express-zod-api';
const getUserEndpoint = endpointsFactory
.build({
methods: ['get'],
input: z.object({
id: z.string().transform((id) => parseInt(id, 10))
}),
output: z.object({
name: z.string(),
}),
handler: async ({input: {id}, options, logger}) => {
logger.debug(`Requested id: ${id}`);
logger.debug('Options:', options);
return { name: 'John Doe' };
}
});
You can add middlewares to the endpoint by using .addMiddleware()
before .build()
.
Set up routing
import {Routing} from 'express-zod-api';
const routing: Routing = {
v1: {
getUser: getUserEndpoint
}
};
This implementation sets up getUserEndpoint
to handle requests to the /v1/getUser
path.
Start your server
import {createServer} from 'express-zod-api';
createServer(config, routing);
Advanced usage
Create a middleware
You can create middlewares separately using createMiddleware()
function and connect them later.
All returns of the connected middlewares are put in options
argument of the endpoint handler.
All middleware inputs are also available as the endpoint inputs.
import {
createMiddleware, z, Method, createHttpError
} from 'express-zod-api';
const methodProviderMiddleware = createMiddleware({
input: z.object({}).nonstrict(),
middleware: async ({request}) => ({
method: request.method.toLowerCase() as Method,
})
});
const authMiddleware = createMiddleware({
input: z.object({
key: z.string().nonempty()
}),
middleware: async ({input: {key}, request, logger}) => {
logger.debug('Checking the key and token...');
if (key !== '123') {
throw createHttpError(401, 'Invalid key');
}
if (request.headers['token'] !== '456') {
throw createHttpError(401, 'Invalid token');
}
return {token: request.headers['token']};
}
});
Refinements
You can also implement the validation inside the input schema:
import {createMiddleware, z} from 'express-zod-api';
const authMiddleware = createMiddleware({
input: z.object({
key: z.string().nonempty()
.refine((key) => key === '123', 'Invalid key')
}),
...
})
Your custom logger
You can specify your custom Winston logger in config:
import * as winston from 'winston';
import {ConfigType, createServer} from 'express-zod-api';
const config: ConfigType = {
logger: winston.createLogger(),
...
};
createServer(config, routing);
Your custom server
You can instantiate your own express app and connect your endpoints the following way. Please note that in this case you probably need to:
- parse
request.body
yourself;
- call
app.listen()
yourself;
- handle
404
errors yourself;
import * as express from 'express';
import {ConfigType, attachRouting} from 'express-zod-api';
const app = express();
const config: ConfigType = {app, ...};
const routing = {...};
attachRouting(config, routing);
app.listen();
Disclosing API specifications
Reusing endpoint types on your frontend
You can export only the types of your endpoints for your front-end:
export type GetUserEndpoint = typeof getUserEndpoint;
Then use provided helpers to obtain their input and output types:
import {EndpointInput, EndpointOutput} from 'express-zod-api';
import {GetUserEndpoint, GetUserEndpoint} from '../your/backend';
type GetUserEndpointInput = EndpointInput<GetUserEndpoint>;
type GetUserEndpointOutput = EndpointOutput<GetUserEndpoint>;
Swagger / OpenAPI Specification
You can generate the specification of your API the following way and write it to a .yaml
file:
import {OpenAPI} from 'express-zod-api';
const yamlString = new OpenAPI({
routing,
version: '1.2.3',
title: 'Example API',
serverUrl: 'http://example.com'
}).builder.getSpecAsYaml();
Known issues
Excess property check of endpoint output
Unfortunately Typescript does not perform excess property check for objects resolved in Promise
, so there is no error during development of endpoint's output.
import {z} from 'express-zod-api';
endpointsFactory.build({
methods, input,
output: z.object({
anything: z.number()
}),
handler: async () => ({
anything: 123,
excessive: 'something'
})
});
You can achieve this check by assigning the output schema to a constant and reusing it in additional definition of handler's return type:
import {z} from 'express-zod-api';
const output = z.object({
anything: z.number()
});
endpointsFactory.build({
methods, input, output,
handler: async (): Promise<z.infer<typeof output>> => ({
anything: 123,
excessive: 'something'
})
});