roman.martynenko@blog
Published on

Documenting API Schemas with Swagger and Zod: A Comprehensive Guide

Authors
  • avatar
    Name
    Roman Martynenko
    Twitter

Introduction

In modern web development, building robust and well-documented APIs is crucial for seamless communication between different parts of your application and third-party services. Effective API documentation helps developers understand how to interact with your API, reduces integration issues, and enhances the overall developer experience.

Swagger and Zod are powerful tools that simplify API documentation and validation. Swagger provides a framework for automatic generation of interactive API documentation, while Zod is a TypeScript-first schema declaration and validation library that ensures your data structures are well-defined and validated at runtime.

In this post, we'll explore how to use Zod to define your API schemas, integrate these schemas with Swagger to generate detailed API documentation, and generate TypeScript types from your Zod schemas. By the end of this tutorial, you'll have a setup that leverages Zod and Swagger, along with TypeScript's type safety.

What You Will Learn

  1. Setting Up the Environment: Preparing your development environment with the necessary tools.
  2. Creating API Schemas with Zod: Defining and validating data structures using Zod.
  3. Integrating Zod with Swagger: Documenting your schemas with Swagger to generate interactive API docs.
  4. Putting It All Together: Building a sample API with Zod, Swagger, and TypeScript.

By the end of this tutorial, you'll understand how to document and validate your APIs using Swagger and Zod, and leverage TypeScript for enhanced development workflow. Let's get started!

Setting Up the Environment

Before we dive into creating and documenting API schemas, we need to set up our development environment. This involves installing Node.js, setting up a new project, and installing the necessary packages.

Prerequisites

Make sure you have the following installed on your machine:

  • Node.js: You can download it from nodejs.org.
  • npm (Node Package Manager) or Yarn: These come with Node.js but you can also install Yarn separately if you prefer.

Initializing a New Project

First, create a new directory for your project and navigate into it:

mkdir zod-swagger-api
cd zod-swagger-api

Initialize a new Node.js project:

npm init -y

This will create a package.json file in your project directory.

Installing Necessary Packages

Next, install the necessary packages for Swagger, Zod, and TypeScript:

npm install express swagger-jsdoc swagger-ui-express zod

To use TypeScript, we also need to install TypeScript and some type definitions:

npm install typescript @types/node @types/express ts-node --save-dev

Setting Up TypeScript

Create a tsconfig.json file to configure TypeScript:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Create a src directory for your TypeScript files:

mkdir src

Creating the Express Server

Create a new file src/index.ts and set up a basic Express server:

import express from 'express';

const app = express();
const port = 5001;

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

To run the server, add the following script to your package.json:

"scripts": {
  "start": "ts-node src/index.ts"
}

Now, start the server:

npm start

You should see the message Server running on http://localhost:5001 in your terminal, and you can visit http://localhost:5001 in your browser to see the "Hello World!" message.

With your environment set up, you're now ready to start creating API schemas with Zod and documenting them with Swagger. In the next section, we'll dive into defining your API schemas using Zod.

Creating API Schemas with Zod

Now that our environment is set up, let's start defining API schemas using Zod. Zod is a powerful TypeScript-first schema declaration and validation library that helps ensure your data structures are well-defined and validated at runtime.

Writing Your First Schema

Let's start by defining a simple schema for a user object. Create a new file src/schemas/user.ts:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

export { UserSchema, User };

In this schema:

  • id is a string that must be a valid UUID.
  • name is a string that must have at least 1 character.
  • email is a string that must be a valid email address.

The z.infer utility is used to generate a TypeScript type from the schema.

Validating Data with Zod

To validate data against the schema, use the safeParse method. Update src/index.ts to include a POST endpoint that validates incoming user data:

import express from 'express';
import { UserSchema } from './schemas/user';

const app = express();
const port = 5001;

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.post('/users', (req, res) => {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error);
  }
  res.status(200).json(result.data);
});

app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});

In this example:

  • We import the UserSchema from our schema file.
  • We create a POST /users endpoint that validates the request body against the UserSchema.
  • If validation fails, we return a 400 status code with the validation errors.
  • If validation succeeds, we return the validated data with a 201 status code.

With this setup, we can ensure that only valid user data is accepted by our API.

In the next section, we'll integrate our Zod schemas with Swagger to generate interactive API documentation.

Integrating Zod with Swagger

To integrate Zod with Swagger, we'll use the swagger-ui-express and @asteasolutions/zod-to-openapi packages. Follow these steps to set up the integration:

Step 1: Install Required Packages

First, install the necessary packages:

npm install swagger-ui-express @asteasolutions/zod-to-openapi
npm install --save-dev @types/swagger-ui-express

Set Up Zod Schemas and OpenAPI Integration

Update Zod schemas for your API in src/schemas/user.ts:

import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';

// Extend Zod with OpenAPI capabilities
extendZodWithOpenApi(z);

export const UserSchema = z.object({
  id: z.string().uuid().openapi({ description: 'Unique identifier for the user' }),
  name: z.string().min(1).openapi({ description: 'Name of the user' }),
  email: z.string().email().openapi({ description: 'Email address of the user' }),
}).openapi({ description: 'User Schema' });
export type User = z.infer<typeof UserSchema>;
  
export const CreateUserSchema = z.object({
  name: z.string().openapi({ description: 'Name of the user' }),
  email: z.string().email().openapi({ description: 'Email address of the user' }),
}).openapi({ description: 'Create User Schema' });
export type CreateUser = z.infer<typeof UserSchema>;

Integrate schemas with OpenAPI using @asteasolutions/zod-to-openapi. Below is an example setup:

import express from 'express';
import swaggerUi from 'swagger-ui-express';
import { z } from 'zod';
import { OpenAPIRegistry, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';

import { CreateUserSchema, UserSchema } from './schemas/user';

// Initialize Express app
const app = express();
app.use(express.json());

// Create OpenAPI registry
const registry = new OpenAPIRegistry();

// Register schemas and paths
registry.registerPath({
  method: 'get',
  path: '/users/{id}',
  description: 'Get a user by ID',
  summary: 'Get User',
  request: {
    params: z.object({ id: z.string() }),
  },
  responses: {
    200: {
      description: 'User found',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
  tags: ['Users'],
});

registry.registerPath({
  method: 'post',
  path: '/users',
  description: 'Create a new user',
  summary: 'Create User',
  request: {
    body: {
      content: {
        'application/json': {
          schema: CreateUserSchema,
        },
      },
    },
  },
  responses: {
    200: {
      description: 'User created',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
  tags: ['Users'],
});

// Generate OpenAPI specification
const generator = new OpenApiGeneratorV3(registry.definitions);
const openApiSpec = generator.generateDocument({
  openapi: '3.0.0',
  info: {
    title: 'User API',
    version: '1.0.0',
  },
});

// Setup Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpec));

// Example endpoints
app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'John Doe', email: 'john.doe@example.com' });
});

app.post('/users', (req, res) => {
    const result = UserSchema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json(result.error);
    }
    res.status(200).json(result.data);
  });

// Start the server
const port = 5001;
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
  console.log(`Swagger UI available at http://localhost:${port}/api-docs`);
});

In this example:

  • We import and extend Zod with OpenAPI capabilities using extendZodWithOpenApi.
  • We define Zod schemas with OpenAPI metadata using .openapi() to provide descriptions.
  • We create an OpenAPIRegistry and register our API paths with corresponding Zod schemas.
  • We generate the OpenAPI specification using OpenApiGeneratorV3.
  • We set up Swagger UI with the generated OpenAPI spec.
  • Finally, we run an Express server with a sample endpoint and Swagger UI documentation.

With this setup, you can visit http://localhost:5001/api-docs to see the interactive API documentation generated by Swagger.

Building a Sample API

Now that we have set up our environment, defined and validated our API schemas with Zod, integrated Zod with Swagger for documentation, let's put everything together to build a sample API.

Defining More Schemas

Let's extend our API with a few more schemas. For example, let's add schemas for Post and Comment.

Create a new file src/schemas/post.ts:

import { z } from 'zod';
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';

// Extend Zod with OpenAPI capabilities
extendZodWithOpenApi(z);

export const CreatePostSchema = z.object({
  title: z.string().min(1).openapi({ description: 'Post title' }),
  content: z.string().min(1).openapi({ description: 'Post content' }),
  authorId: z.string().uuid().openapi({ description: 'Post author unique identifier' }),
});
export const PostSchema = CreatePostSchema.extend({
  id: z.string().uuid().openapi({ description: 'Unique identifier for the post' }),
});
export type CreatePost = z.infer<typeof CreatePostSchema>;
export type Post = z.infer<typeof PostSchema>;

export const CreateCommentSchema = z.object({
  postId: z.string().uuid().openapi({ description: 'Unique identifier for the post where comment belongs' }),
  content: z.string().min(1).openapi({ description: 'Comment text' }),
  authorId: z.string().uuid().openapi({ description: 'Unique identifier for the author' }),
});
export const CommentSchema = CreateCommentSchema.extend({
  id: z.string().uuid().openapi({ description: 'Unique identifier for the comment' }),
});
export type CreateComment = z.infer<typeof CreateCommentSchema>;
export type Comment = z.infer<typeof CommentSchema>;

Updating the API Endpoints

Update src/index.ts to add endpoints for creating and retrieving posts and comments:

import express from 'express';
import swaggerUi from 'swagger-ui-express';
import { z } from 'zod';
import { OpenAPIRegistry, OpenApiGeneratorV3 } from '@asteasolutions/zod-to-openapi';

import { CreateUserSchema, UserSchema } from './schemas/user';
import { CommentSchema, CreateCommentSchema, CreatePostSchema, Post, PostSchema, Comment, CreateComment, CreatePost } from './schemas/post';

// Initialize Express app
const app = express();
app.use(express.json());

// Create OpenAPI registry
const registry = new OpenAPIRegistry();

// Register schemas and paths
registry.registerPath({
  method: 'get',
  path: '/users/{id}',
  description: 'Get a user by ID',
  summary: 'Get User',
  request: {
    params: z.object({ id: z.string() }),
  },
  responses: {
    200: {
      description: 'User found',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
  tags: ['Users'],
});

registry.registerPath({
  method: 'post',
  path: '/users',
  description: 'Create a new user',
  summary: 'Create User',
  request: {
    body: {
      content: {
        'application/json': {
          schema: CreateUserSchema,
        },
      },
    },
  },
  responses: {
    200: {
      description: 'User created',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
  tags: ['Users'],
});

registry.registerPath({
  method: 'post',
  path: '/posts',
  description: 'Create a new post',
  summary: 'Create post',
  request: {
    body: {
      content: {
        'application/json': {
          schema: CreatePostSchema,
        },
      },
    },
  },
  responses: {
    200: {
      description: 'Created post',
      content: {
        'application/json': {
          schema: PostSchema,
        },
      },
    },
  },
  tags: ['Posts'],
});

registry.registerPath({
  method: 'post',
  path: '/comments',
  description: 'Create a new comment',
  summary: 'Create comment',
  request: {
    body: {
    content: {
      'application/json': {
        schema: CreateCommentSchema,
      },
    },
    },
  },
  responses: {
    200: {
      description: 'Created post',
      content: {
        'application/json': {
          schema: CommentSchema,
       },
      },
    },
  },
  tags: ['Comments'],
});

// Generate OpenAPI specification
const generator = new OpenApiGeneratorV3(registry.definitions);
const openApiSpec = generator.generateDocument({
  openapi: '3.0.0',
  info: {
    title: 'User API',
    version: '1.0.0',
  },
});

// Setup Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpec));

// Example endpoints
app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id, name: 'John Doe', email: 'john.doe@example.com' });
});

app.post('/users', (req, res) => {
  const result = CreateUserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error);
  }
  res.status(200).json({ ...result.data, id: 'a779e093-1fe2-43e7-9fa7-878ed00852ff' });
});

app.post('/posts', (req, res) => {
  const result = CreatePostSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error);
  }
  const post: CreatePost = result.data;
  res.status(200).json({ ...post, id: '2a6546c5-5481-4dd0-8975-d3ab888506b7' });
});

app.post('/comments', (req, res) => {
  const result = CreateCommentSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(400).json(result.error);
  }
  const comment: CreateComment = result.data;
  res.status(200).json({ ...comment, id: 'fd41790f-cfb9-40ad-9dba-51380f90706a' });
});

// Start the server
const port = 5001;
app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
  console.log(`Swagger UI available at http://localhost:${port}/api-docs`);
});

In this example, we:

  • Create PostSchema and CommentSchema
  • Use zod extend method to avoid code duplication
  • Import the PostSchema and CommentSchema.
  • Add endpoints for creating posts and comments.
  • Update the OpenAPI document to include these new endpoints.
  • Add API handlers for new endpoints

With this setup, you can visit http://localhost:5001/api-docs to see the updated interactive API documentation generated by Swagger.

Congratulations! You've successfully built a sample API using Zod, Swagger, and TypeScript. Your API is now well-documented, type-safe, and ready for further development. application

Areas for improvement

Code from this tutorial is not meant to be production-ready and has number of things to be improved:

  • Move swagger and zod configuration from src/index.ts into separate file(s)
  • zod library has enormous capabilities, we've covered only basics
  • It is better to have a dedicated file for each api handler for easier developing, testing and maintaining
  • Real application would typically have additional abstraction layer (e.g for error handling)

Sources