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

May 9, 2025 - 13:31
 0
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) // 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.