Create a Node.js REST API with an OpenAPI description in minutes
@apexjs-org/openapi is an OpenAPI 3.1+ description library for TypeScript. You can use this package to easily create a type-safe OpenAPI (Swagger) description with Zod schema support in Node.js. In this tutorial, we use express-openapi-validator to bring the OpenAPI description to life with automatic validation and request handling. Step 1. Install the packages Install the following packages: npm install @apexjs-org/openapi express express-openapi-validator zod To configure TypeScript, install: npm install --save-dev typescript ts-node @types/node And create a file named tsconfig.json: { "compilerOptions": { "declaration": true, "target": "es2017", "module": "nodenext", "moduleResolution": "nodenext", "outDir": "dist", "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true }, "exclude": [ "node_modules" ], "include": [ "src/**/*.ts", ] } Create a /src folder and place all files below inside it. Step 2. Define your schemas Create a file named schemas.ts and define your API response and request schemas as Zod schemas or JSON schemas: // schemas.ts import { z } from "zod"; export const User = z.object({ id: z.string().regex(/^[a-zA-Z0-9-_]+$/).min(10).max(200), email: z.string().email().min(5).max(200), name: z.string().regex(/^[a-zA-Z0-9-_ ]+$/).min(2).max(200), createdAt: z.optional(z.date()), }); export const UserList = z.object({ results: z.array(User), totalCount: z.number() }); export const UserCreate = User.omit({ id: true, createdAt: true }); export const UserUpdate = User.pick({ name: true }).partial(); Step 3. Define your paths Create a file named paths.ts and define your API paths as specified in the OpenAPI 3.1 specification, with shorthands: // paths.ts import { type Paths, searchParameterRefs, jsonResponse, errorResponseRefs, jsonBody, idParameters, schemaRef } from "@apexjs-org/openapi"; const paths: Paths = {} // Methods for the /users path paths['/users'] = { get: { operationId: 'listUsers', // Name of the function that this request should trigger summary: 'Finds users.', parameters: searchParameterRefs(), // References the q, sort and offset parameters (included in components.parameters, see openapi.ts below) responses: { ...errorResponseRefs(), // References the BadRequest, Unauthorized, Forbidden, NotFound and TooManyRequests errors (included in components.responses, see openapi.ts below) '200': jsonResponse(schemaRef('UserList')) // JSON response with a reference to a custom schema (included in components.schemas, see openapi.ts below) } }, post: { operationId: 'createUser', summary: 'Creates a new user.', requestBody: jsonBody(schemaRef('UserCreate')), // JSON body with a reference to a custom schema (included in components.schemas, see openapi.ts below) responses: { ...errorResponseRefs(), '201': jsonResponse(schemaRef('User'), 'created') } } }; // Methods for the /users/{userId} path paths['/users/{userId}'] = { get: { operationId: 'getUser', summary: 'Gets a user by id.', parameters: idParameters(['userId']), // Specifies the userId parameter in this path responses: { ...errorResponseRefs(), '200': jsonResponse(schemaRef('User')) } }, patch: { operationId: 'updateUser', summary: 'Updates a user by id.', parameters: idParameters(['userId']), requestBody: jsonBody(schemaRef('UserUpdate')), responses: { ...errorResponseRefs(), '200': jsonResponse(schemaRef('User')) } }, delete: { operationId: 'deleteUser', summary: 'Deletes a user by id.', parameters: idParameters(['userId']), responses: { ...errorResponseRefs(), '200': jsonResponse() // JSON response without a schema (reference) } } }; export const userPaths = paths; Step 4. Define the OpenAPI description Create a file named openapi.ts and define your API as specified in the OpenAPI 3.1 specification. Use the schemas, paths and shorthands: // openapi.ts import { type OpenApi, bearerScheme, errorResponses, searchParameters, jsonSchemas, errorSchema } from "@apexjs-org/openapi"; import * as schemas from "./schemas.js"; import { userPaths } from "./paths.js"; export const openapi: OpenApi = { openapi: '3.1.0', info: { title: 'API title', version: '1.0.0' }, security: [ // { BearerAuth: [] } // Uncomment this to enable security/the BearerAuth security scheme in all paths, see components.securitySchemes. Specifying security at the path method level is possible as well (to disable global security on path level, use: security: []) ], paths: userPaths, components: { schemas: { Error: errorSchema(), // Specifies the Error schema for the error responses, same schema as express-openapi-validator errors ...jsonSchemas(schemas) // Convert

@apexjs-org/openapi is an OpenAPI 3.1+ description library for TypeScript. You can use this package to easily create a type-safe OpenAPI (Swagger) description with Zod schema support in Node.js. In this tutorial, we use express-openapi-validator to bring the OpenAPI description to life with automatic validation and request handling.
Step 1. Install the packages
Install the following packages:
npm install @apexjs-org/openapi express express-openapi-validator zod
To configure TypeScript, install:
npm install --save-dev typescript ts-node @types/node
And create a file named tsconfig.json:
{
"compilerOptions": {
"declaration": true,
"target": "es2017",
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "dist",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true
},
"exclude": [
"node_modules"
],
"include": [
"src/**/*.ts",
]
}
Create a /src folder and place all files below inside it.
Step 2. Define your schemas
Create a file named schemas.ts and define your API response and request schemas as Zod schemas or JSON schemas:
// schemas.ts
import { z } from "zod";
export const User = z.object({
id: z.string().regex(/^[a-zA-Z0-9-_]+$/).min(10).max(200),
email: z.string().email().min(5).max(200),
name: z.string().regex(/^[a-zA-Z0-9-_ ]+$/).min(2).max(200),
createdAt: z.optional(z.date()),
});
export const UserList = z.object({
results: z.array(User),
totalCount: z.number()
});
export const UserCreate = User.omit({ id: true, createdAt: true });
export const UserUpdate = User.pick({ name: true }).partial();
Step 3. Define your paths
Create a file named paths.ts and define your API paths as specified in the OpenAPI 3.1 specification, with shorthands:
// paths.ts
import { type Paths, searchParameterRefs, jsonResponse, errorResponseRefs, jsonBody, idParameters, schemaRef } from "@apexjs-org/openapi";
const paths: Paths = {}
// Methods for the /users path
paths['/users'] = {
get: {
operationId: 'listUsers', // Name of the function that this request should trigger
summary: 'Finds users.',
parameters: searchParameterRefs(), // References the q, sort and offset parameters (included in components.parameters, see openapi.ts below)
responses: {
...errorResponseRefs(), // References the BadRequest, Unauthorized, Forbidden, NotFound and TooManyRequests errors (included in components.responses, see openapi.ts below)
'200': jsonResponse(schemaRef('UserList')) // JSON response with a reference to a custom schema (included in components.schemas, see openapi.ts below)
}
},
post: {
operationId: 'createUser',
summary: 'Creates a new user.',
requestBody: jsonBody(schemaRef('UserCreate')), // JSON body with a reference to a custom schema (included in components.schemas, see openapi.ts below)
responses: {
...errorResponseRefs(),
'201': jsonResponse(schemaRef('User'), 'created')
}
}
};
// Methods for the /users/{userId} path
paths['/users/{userId}'] = {
get: {
operationId: 'getUser',
summary: 'Gets a user by id.',
parameters: idParameters(['userId']), // Specifies the userId parameter in this path
responses: {
...errorResponseRefs(),
'200': jsonResponse(schemaRef('User'))
}
},
patch: {
operationId: 'updateUser',
summary: 'Updates a user by id.',
parameters: idParameters(['userId']),
requestBody: jsonBody(schemaRef('UserUpdate')),
responses: {
...errorResponseRefs(),
'200': jsonResponse(schemaRef('User'))
}
},
delete: {
operationId: 'deleteUser',
summary: 'Deletes a user by id.',
parameters: idParameters(['userId']),
responses: {
...errorResponseRefs(),
'200': jsonResponse() // JSON response without a schema (reference)
}
}
};
export const userPaths = paths;
Step 4. Define the OpenAPI description
Create a file named openapi.ts and define your API as specified in the OpenAPI 3.1 specification. Use the schemas, paths and shorthands:
// openapi.ts
import { type OpenApi, bearerScheme, errorResponses, searchParameters, jsonSchemas, errorSchema } from "@apexjs-org/openapi";
import * as schemas from "./schemas.js";
import { userPaths } from "./paths.js";
export const openapi: OpenApi = {
openapi: '3.1.0',
info: {
title: 'API title',
version: '1.0.0'
},
security: [
// { BearerAuth: [] } // Uncomment this to enable security/the BearerAuth security scheme in all paths, see components.securitySchemes. Specifying security at the path method level is possible as well (to disable global security on path level, use: security: [])
],
paths: userPaths,
components: {
schemas: {
Error: errorSchema(), // Specifies the Error schema for the error responses, same schema as express-openapi-validator errors
...jsonSchemas(schemas) // Converts Zod schemas to JSON schemas
},
parameters: searchParameters(), // Specifies the q, sort and offset parameters so that they can be referenced
securitySchemes: {
BearerAuth: bearerScheme() // Specifies a bearer security scheme. openIdScheme() and oauth2Scheme() are possible as well
},
responses: errorResponses()
}
}
// console.dir(openapi, { depth: null }) // Use this to view the OpenAPI description in your console
Step 5. Create middleware for Express
Express is a popular web framework for NodeJs. We can transform the OpenAPI description into a REST API by using express-openapi-validator. This packages auto-validates and handles your requests. Create a file named middleware.ts to piece things together.
// middleware.ts
import * as OpenApiValidator from 'express-openapi-validator';
// express openapi validator middleware
export function validator(apiSpec: any, operations: any, securityHandlers: any = {}, options: any = {}) {
return OpenApiValidator.middleware({
apiSpec,
validateRequests: { removeAdditional: 'all' },
operationHandlers: {
basePath: '/operations',
resolver: function (basePath, route, apiDoc) {
const pathKey = route.openApiRoute.substring(route.basePath.length);
const schema = (apiDoc.paths || {})[pathKey][route.method.toLowerCase()];
return operations[schema?.operationId];
}
},
validateSecurity: {
handlers: { ...securityHandlers } // e.g. { BearerAuth: async (req, scopes, schema) => {}}
},
...options
});
}
// express middleware that handles errors
export function errorHandler(err: any, req: any, res: any, next: any) {
res.status(err.status || 500).json({
message: err.status ? err.message : 'internal server error',
errors: err.status ? err.errors : undefined
});
}
// security handler for bearerAuth
export async function BearerAuth(req, scopes, schema) {
// To do: verify user logic
// throw Error('You are not signed in');
}
Step 6. Create the operation handlers
Operation handlers are functions that express-openapi-validator triggers when a request is valid. They are specified as operationId in the OpenAPI paths, see above. Operation handlers behave the same as Express route handlers. Create a file named operations.ts and define the functions:
// operations.ts
export async function listUsers(req, res, next) {
res.status(200).json({
results: [], // To do: ouptut JSON according to the User schema
totalCount: 0
})
}
export const createUser = [
async (req, res, next) => { next(); },
async (req, res, next) => { res.status(200).json({}) }, // To do: ouptut JSON according to the User schema
]
export async function getUser(req, res, next) {
res.status(200).json({}) // To do: ouptut JSON according to the User schema
}
export async function updateUser(req, res, next) {
res.status(200).json({}) // To do: ouptut JSON according to the User schema
}
export async function deleteUser(req, res, next) {
res.status(200).json({})
}
Step 7. Start the server
Create a file named index.ts and enjoy your REST API.
// index.ts
import express from 'express';
import { validator, errorHandler, BearerAuth } from './middleware.js';
import { openapi } from './openapi.js';
import * as operations from './operations.js';
const start = async () => {
try {
const app = express();
const securityHandlers = {}; // = no security/authentication, to enable security use { BearerAuth } and uncomment openapi.ts security
app.use(express.json());
app.use(validator(openapi, operations, securityHandlers));
app.use(errorHandler);
app.listen(8080, () => console.log('Running...'));
}
catch (err) {
console.log(err);
}
}
start();
Use the following command to start the server in an development environment:
ts-node-esm ./src/index.ts
That's it
Now, you can send your API requests to http://localhost:8080
To improve the security (headers) of your API, consider packages like helmet, cors and express-rate-limit.