API documentation is crucial for any backend service, and Swagger/OpenAPI has become the de facto standard for documenting REST APIs. However, maintaining documentation manually can be time-consuming and error-prone. This guide explores how to automatically generate Swagger documentation from Joi validation schemas using the joi-to-swagger library, ensuring your API documentation stays in sync with your validation logic.

When implementing API documentation, it’s important to consider health checks and graceful shutdown for your Express application, especially in production environments.

swagger documentation

Understanding the Components

Before diving into implementation, let’s understand the key components:

  1. Joi Validation:

    • Schema-based validation library
    • Defines data structure and validation rules
    • Used for request/response validation
  2. Swagger/OpenAPI:

    • Standard specification for REST API documentation
    • Provides interactive documentation
    • Supports testing endpoints directly
  3. joi-to-swagger:

    • Converts Joi schemas to Swagger/OpenAPI format
    • Maintains consistency between validation and documentation
    • Reduces documentation maintenance overhead

Setting Up the Environment

First, let’s set up our project with the necessary dependencies:

1
2
3
4
5
6
7
8
9
// package.json
{
  "dependencies": {
    "express": "^4.18.2",
    "joi": "^17.11.0",
    "joi-to-swagger": "^3.6.0",
    "swagger-ui-express": "^5.0.0"
  }
}

Install the dependencies:

1
2
npm install express joi --save
npm install joi-to-swagger swagger-ui-express --save-dev // Save as dev dependency

Creating Validation Schemas

Let’s create a sample API with validation schemas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const Joi = require('joi');

// User creation schema
const createUserSchema = Joi.object({
  username: Joi.string().min(3).max(30).required()
    .description('Username must be between 3 and 30 characters'),
  email: Joi.string().email().required()
    .description('Valid email address'),
  age: Joi.number().integer().min(18).max(120)
    .description('Age must be between 18 and 120')
});

// User response schema
const userResponseSchema = Joi.object({
  id: Joi.string().uuid().required(),
  username: Joi.string().required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer(),
  createdAt: Joi.date().iso()
});

Generating Swagger Documentation

Now, let’s convert our Joi schemas to Swagger format:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const { joiToSwagger } = require('joi-to-swagger');

// Convert schemas to Swagger components
const swaggerComponents = {
  schemas: {
    CreateUser: joiToSwagger(createUserSchema).swagger,
    UserResponse: joiToSwagger(userResponseSchema).swagger
  }
};

// Create Swagger documentation
const swaggerDocument = {
  openapi: '3.0.0',
  info: {
    title: 'User Management API',
    version: '1.0.0',
    description: 'API for managing users'
  },
  paths: {
    '/users': {
      post: {
        summary: 'Create a new user',
        tags: ['Users'],
        requestBody: {
          required: true,
          content: {
            'application/json': {
              schema: {
                $ref: '#/components/schemas/CreateUser'
              }
            }
          }
        },
        responses: {
          '201': {
            description: 'User created successfully',
            content: {
              'application/json': {
                schema: {
                  $ref: '#/components/schemas/UserResponse'
                }
              }
            }
          }
        }
      }
    }
  },
  components: swaggerComponents
};

Integrating with Express

Let’s set up the Express application with Swagger UI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express');
const swaggerUi = require('swagger-ui-express');

const app = express();
app.use(express.json());

// Serve Swagger documentation
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

// User creation endpoint
app.post('/users', (req, res) => {
  const { error, value } = createUserSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  
  // Create user logic here
  res.status(201).json({
    id: '123e4567-e89b-12d3-a456-426614174000',
    ...value,
    createdAt: new Date().toISOString()
  });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
  console.log('API Documentation available at http://localhost:3000/api-docs');
});

Note: API Documentation available at http://localhost:3000/api-docs

Best Practices

1. Schema Organization

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ❌ Bad Practice
const userSchema = Joi.object({
  // All fields in one schema
});

// ✅ Good Practice
const baseUserSchema = {
  username: Joi.string().min(3).max(30).required(),
  email: Joi.string().email().required()
};

const createUserSchema = Joi.object({
  ...baseUserSchema,
  password: Joi.string().min(8).required()
});

const updateUserSchema = Joi.object({
  ...baseUserSchema,
  age: Joi.number().integer().min(18)
});

2. Documentation Enhancement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ❌ Bad Practice
const schema = Joi.object({
  name: Joi.string().required()
});

// ✅ Good Practice
const schema = Joi.object({
  name: Joi.string().required()
    .description('User\'s full name')
    .example('John Doe')
    .meta({
      swagger: {
        description: 'The user\'s full name as it appears on official documents'
      }
    })
});

3. Error Handling

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ❌ Bad Practice
app.post('/users', (req, res) => {
  const { error } = schema.validate(req.body);
  if (error) {
    res.status(400).send(error.message);
  }
});

// ✅ Good Practice
app.post('/users', (req, res) => {
  const { error, value } = schema.validate(req.body, {
    abortEarly: false,
    stripUnknown: true
  });
  
  if (error) {
    return res.status(400).json({
      status: 'error',
      message: 'Validation failed',
      details: error.details.map(detail => ({
        field: detail.path.join('.'),
        message: detail.message
      }))
    });
  }
});

Advanced Features

1. Custom Swagger Extensions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const schema = Joi.object({
  userId: Joi.string().uuid()
    .meta({
      swagger: {
        'x-codegen-request-body-name': 'user',
        'x-codegen-request-body-example': {
          userId: '123e4567-e89b-12d3-a456-426614174000'
        }
      }
    })
});

2. Security Definitions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const swaggerDocument = {
  // ... other configurations
  components: {
    securitySchemes: {
      bearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT'
      }
    }
  },
  security: [{
    bearerAuth: []
  }]
};

For production deployments, make sure to follow best practices for production setup and implement proper health checks to ensure your API remains reliable and maintainable.

Conclusion

Using joi-to-swagger to generate API documentation provides several benefits:

  1. Single source of truth for validation and documentation
  2. Reduced maintenance overhead
  3. Consistent API documentation
  4. Interactive testing capabilities
  5. Better developer experience

By following the best practices outlined in this guide, you can create comprehensive and maintainable API documentation that stays in sync with your validation logic.

Key takeaways:

  1. Use Joi schemas as the single source of truth
  2. Enhance schemas with descriptive metadata
  3. Organize schemas for reusability
  4. Implement proper error handling
  5. Leverage advanced Swagger features

✨ Thank you for reading! I welcome your feedback and questions in the comments section.