DEV Community

1xApi
1xApi

Posted on • Edited on • Originally published at 1xapi.com

How to Build Self-Documenting APIs with OpenAPI 3.1 in Node.js (2026 Guide)

How to Build Self-Documenting APIs with OpenAPI 3.1 in Node.js (2026 Guide)

As of February 2026, API documentation remains one of the most neglected aspects of backend development. Yet poor documentation is the #1 complaint from developers consuming APIs. The solution? Self-documenting APIs that generate documentation directly from your code.

In this guide, you'll learn how to build APIs that document themselves using OpenAPI 3.1 — the latest version of the OpenAPI Specification (OAS).

What is OpenAPI 3.1?

OpenAPI 3.1 is the newest version of the industry-standard specification for describing REST APIs. Unlike its predecessors, OpenAPI 3.1 introduces full JSON Schema compliance, making it easier to define and validate request/response schemas.

Key improvements in OpenAPI 3.1:

  • Full JSON Schema 2020-12 support
  • Improved array handling with prefixItems
  • Better discriminator support for polymorphic schemas
  • Enhanced security scheme definitions

Setting Up Your Project

Let's build a sample API with automatic OpenAPI documentation. We'll use Fastify with @fastify/swagger — the most popular combination in 2026.

mkdir openapi-demo && cd openapi-demo
npm init -y
npm install fastify @fastify/swagger @fastify/swagger-ui @fastify/type-provider-typebox
Enter fullscreen mode Exit fullscreen mode

Create server.js:

const fastify = require('fastify')({ logger: true });
const swagger = require('@fastify/swagger');
const swaggerUi = require('@fastify/swagger-ui');
const { Type, Static } = require('@sinclair/typebox');

// Define schemas using TypeBox (TypeScript-like schema definition)
const UserSchema = Type.Object({
  id: Type.String(),
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: 'email' }),
  role: Type.Union([Type.Literal('admin'), Type.Literal('user')]),
  createdAt: Type.String({ format: 'date-time' })
});

const CreateUserSchema = Type.Object({
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: 'email' }),
  password: Type.String({ minLength: 8 })
});

const UserResponseSchema = Type.Object({
  user: UserSchema,
  message: Type.String()
});

// In-memory storage (replace with database in production)
const users = new Map();
let userIdCounter = 1;

// Register Swagger
fastify.register(swagger, {
  openapi: {
    info: {
      title: 'User Management API',
      description: 'A sample API demonstrating OpenAPI 3.1 self-documentation',
      version: '1.0.0',
      contact: {
        name: 'API Support',
        email: 'support@example.com'
      }
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: 'Development server'
      }
    ],
    tags: [
      { name: 'Users', description: 'User management endpoints' }
    ]
  }
});

fastify.register(swaggerUi, {
  routePrefix: '/docs'
});

// Routes with inline documentation
fastify.get('/users', {
  schema: {
    tags: ['Users'],
    summary: 'List all users',
    description: 'Retrieve a list of all registered users',
    response: {
      200: Type.Array(UserSchema)
    }
  }
}, async (request, reply) => {
  return Array.from(users.values());
});

fastify.get('/users/:id', {
  schema: {
    tags: ['Users'],
    summary: 'Get user by ID',
    description: 'Retrieve a specific user by their unique identifier',
    params: Type.Object({
      id: Type.String()
    }),
    response: {
      200: UserSchema,
      404: Type.Object({
        statusCode: Type.Number(),
        error: Type.String(),
        message: Type.String()
      })
    }
  }
}, async (request, reply) => {
  const user = users.get(request.params.id);
  if (!user) {
    reply.code(404);
    return { statusCode: 404, error: 'Not Found', message: 'User not found' };
  }
  return user;
});

fastify.post('/users', {
  schema: {
    tags: ['Users'],
    summary: 'Create a new user',
    description: 'Register a new user in the system',
    body: CreateUserSchema,
    response: {
      201: UserResponseSchema
    }
  }
}, async (request, reply) => {
  const { name, email, password } = request.body;

  const id = String(userIdCounter++);
  const user = {
    id,
    name,
    email,
    role: 'user',
    createdAt: new Date().toISOString()
  };

  users.set(id, user);

  reply.code(201);
  return { user, message: 'User created successfully' };
});

fastify.delete('/users/:id', {
  schema: {
    tags: ['Users'],
    summary: 'Delete a user',
    description: 'Remove a user from the system',
    params: Type.Object({
      id: Type.String()
    }),
    response: {
      204: Type.Optional(Type.Null()),
      404: Type.Object({
        statusCode: Type.Number(),
        error: Type.String(),
        message: Type.String()
      })
    }
  }
}, async (request, reply) => {
  const { id } = request.params;

  if (!users.has(id)) {
    reply.code(404);
    return { statusCode: 404, error: 'Not Found', message: 'User not found' };
  }

  users.delete(id);
  reply.code(204);
});

// Start server
const start = async () => {
  try {
    await fastify.listen({ port: 3000 });
    console.log('Server running at http://localhost:3000');
    console.log('API docs available at http://localhost:3000/docs');
  } catch (err) {
    fastify.log.error(err);
    process.exit(1);
  }
};

start();
Enter fullscreen mode Exit fullscreen mode

Running and Testing

node server.js
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost/3000/docs — you'll see an interactive Swagger UI where developers can:

  • Browse all endpoints
  • See request/response schemas
  • Test endpoints directly from the browser
  • Export the OpenAPI specification as JSON or YAML

Why This Matters in 2026

1. Zero Documentation Drift
Your docs always match your code because they come from the same schemas.

2. Better Developer Experience
Interactive docs reduce support burden and accelerate API adoption.

3. Automatic Client SDK Generation
Tools like openapi-generator can create client libraries from your spec.

4. Contract Testing
Validate that your API actually conforms to its specification.

Advanced: Export and Share

Generate a standalone OpenAPI file:

// Add this route to export the spec
fastify.get('/openapi.json', async (request, reply) => {
  return fastify.swagger();
});
Enter fullscreen mode Exit fullscreen mode

Now you can share http://localhost:3000/openapi.json with API consumers or import it into tools like:

  • Postman (import directly)
  • Stoplight Studio (design & mock)
  • Redoc (beautiful static docs)
  • Scalar (modern API explorer)

Conclusion

Self-documenting APIs aren't a luxury — they're a necessity in 2026. By defining schemas alongside your routes, you get:

  • ✅ Always-up-to-date documentation
  • ✅ Request/response validation for free
  • ✅ Interactive API explorer for developers
  • ✅ Exportable spec for tooling

The setup takes minutes, and the ROI is immediate. Your API consumers will thank you.


Ready to level up your API? Check out our other tutorials on API versioning, error handling, and authentication best practices.

Top comments (0)