js

Why Great API Documentation Matters—and How to Build It with TypeScript

Discover how to create accurate, maintainable API documentation using TypeScript, decorators, and real-world examples. Improve dev experience today.

Why Great API Documentation Matters—and How to Build It with TypeScript

I’ve been building APIs for years, and I’ve seen firsthand how documentation can make or break a project. Just last week, I spent three hours trying to integrate with an API that had outdated documentation. The endpoints had changed, the response formats were different, and the authentication method was completely wrong. That frustration is exactly why I’m writing this today. Good documentation isn’t just nice to have—it’s essential for any API that people actually need to use.

Think about it: when was the last time you enjoyed working with a poorly documented API? Probably never. The truth is, your API is only as good as its documentation. If developers can’t figure out how to use it, they’ll find another solution. That’s why I’ve made it my mission to build documentation that stays in sync with the code, provides real examples, and actually helps people get their work done.

Let’s start with the basics. You need to set up your project properly from the beginning. This isn’t just about installing packages—it’s about creating a structure that supports good documentation practices. I always begin with a clean TypeScript setup because it gives us type safety and better tooling.

mkdir api-documentation-project
cd api-documentation-project
npm init -y

Now, let’s install what we actually need. Notice I’m not just throwing every possible package at this. Each one serves a specific purpose in our documentation strategy.

npm install express
npm install swagger-ui-express swagger-jsdoc
npm install class-validator class-transformer
npm install reflect-metadata
npm install -D typescript @types/node @types/express

Have you ever wondered why some API documentation feels outdated the moment it’s published? It’s usually because it was written separately from the code. The developers built the API, then someone else wrote documentation based on what they thought the API did. This creates a gap that only grows over time.

The solution is what I call “documentation as code.” Your documentation lives right alongside your implementation. When you change the code, you change the documentation in the same commit. This approach has saved me countless hours of maintenance headaches.

Let me show you how I structure my TypeScript configuration. This isn’t just about making TypeScript work—it’s about enabling the features that make documentation generation possible.

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strict": true
  }
}

Those last two options are crucial. They allow us to use decorators—special annotations that attach metadata to our code. This metadata becomes the foundation of our documentation. Without it, we’d have to write everything twice: once in code, once in documentation.

Now, let’s talk about models. In TypeScript, we define what our data looks like using interfaces and classes. But did you know you can make these definitions work double duty? They can both validate incoming requests and generate documentation.

Here’s how I define a user model:

/**
 * A user in our system
 */
export class User {
  /**
   * Unique identifier
   * @example "550e8400-e29b-41d4-a716-446655440000"
   */
  @IsUUID()
  id: string;

  /**
   * User's email address
   * @example "[email protected]"
   */
  @IsEmail()
  email: string;

  /**
   * User's full name
   * @example "John Doe"
   */
  @IsString()
  @MinLength(1)
  @MaxLength(100)
  name: string;
}

See what’s happening here? The decorators like @IsEmail() and @IsUUID() do two things. First, they validate that incoming data matches what we expect. Second, they tell our documentation generator what the requirements are for each field. The comments with @example provide concrete examples that will appear in the documentation.

But what about endpoints? How do we document those? This is where controller decorators come in. They let us describe what each endpoint does, what it expects, and what it returns—all in the same file where we write the actual logic.

Here’s a user controller example:

@Controller('/users')
export class UserController {
  /**
   * Get a user by ID
   */
  @Get('/:id')
  @ResponseSchema(User)
  async getUser(
    @Param('id') id: string
  ) {
    // Your actual logic here
    return await userService.findById(id);
  }

  /**
   * Create a new user
   */
  @Post('/')
  @ResponseSchema(User, { statusCode: 201 })
  async createUser(
    @Body() createUserDto: CreateUserDto
  ) {
    // Your actual logic here
    return await userService.create(createUserDto);
  }
}

Notice how clean this is? The documentation is right there with the code. If I change how the endpoint works, I update the decorators and comments at the same time. No separate documentation file to forget about.

Now, here’s a question for you: what happens when your API needs to handle errors? This is something many documentation approaches get wrong. They show the happy path but forget about all the ways things can go wrong.

I always include error responses in my documentation. Here’s how:

/**
 * Get a user by ID
 * @throws {404} User not found
 * @throws {500} Internal server error
 */
@Get('/:id')
@ResponseSchema(User)
@ResponseSchema(ErrorResponse, { statusCode: 404 })
@ResponseSchema(ErrorResponse, { statusCode: 500 })
async getUser(@Param('id') id: string) {
  const user = await userService.findById(id);
  if (!user) {
    throw new NotFoundError('User not found');
  }
  return user;
}

This tells developers exactly what to expect when things don’t go perfectly. They know what status codes to handle and what the error responses will look like. This level of detail separates professional documentation from amateur attempts.

But documentation isn’t just about what’s possible—it’s also about what’s not allowed. Security is a critical part of any API, and your documentation should reflect that. How do you document authentication and authorization?

I use security decorators to make this clear:

/**
 * Update user profile
 */
@Put('/profile')
@Security('bearer')
@ResponseSchema(User)
async updateProfile(
  @CurrentUser() user: User,
  @Body() updateDto: UpdateProfileDto
) {
  return await userService.updateProfile(user.id, updateDto);
}

The @Security('bearer') decorator tells developers that this endpoint requires a bearer token. It also tells our documentation generator to include authentication information in the generated docs. This means developers can see at a glance which endpoints require authentication and what type they need.

Now, let’s talk about something practical: testing your documentation. How do you know your documentation actually matches what your API does? I’ve been burned by this before—documentation that looks great but describes an API that doesn’t exist.

Here’s my solution: I write tests that verify my documentation. These tests make sure that the examples in my documentation actually work with my API. If I change my API in a way that breaks the examples, the tests fail.

describe('API Documentation', () => {
  it('should match actual user creation', async () => {
    const exampleUser = {
      email: '[email protected]',
      name: 'Test User',
      password: 'SecurePass123!'
    };

    // This example comes from our documentation
    const response = await request(app)
      .post('/users')
      .send(exampleUser);

    expect(response.status).toBe(201);
    expect(response.body).toMatchObject({
      email: exampleUser.email,
      name: exampleUser.name
    });
  });
});

This test does something important: it ensures that the examples I provide in documentation are actually valid. If the API changes and the example no longer works, this test will fail. This gives me confidence that my documentation is accurate.

But what about keeping documentation up to date across multiple versions? This is where many teams struggle. You release version 2.0 of your API, but your documentation still shows version 1.0 endpoints.

I solve this with versioned documentation. Each version of my API gets its own documentation set, and I use routing to serve the correct version based on the request.

// Serve v1 documentation
app.use('/api/v1/docs', swaggerUi.serve, swaggerUi.setup(v1Spec));

// Serve v2 documentation
app.use('/api/v2/docs', swaggerUi.serve, swaggerUi.setup(v2Spec));

// Serve latest documentation
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(latestSpec));

This approach means developers can always find the documentation for the version they’re using. They can also see what’s changed between versions, which helps with migration planning.

Here’s something else I’ve learned: good documentation includes more than just endpoint descriptions. It needs to explain concepts, provide context, and guide developers through common tasks. That’s why I always include a “Getting Started” section in my documentation.

But how do you generate that from code? I create special files that contain this guidance and include them in my documentation generation process. These files use the same format as my code documentation, so everything stays consistent.

/**
 * # Getting Started
 * 
 * Welcome to our API! This guide will help you make your first request.
 * 
 * ## Authentication
 * 
 * Most endpoints require authentication. Here's how to get started:
 * 
 * 1. Register for an account at /auth/register
 * 2. Get your API key from the dashboard
 * 3. Include the key in the `Authorization` header
 * 
 * ## Making Your First Request
 * 
 * Here's an example using curl:
 * 
 * ```bash
 * curl -H "Authorization: Bearer YOUR_KEY" \
 *   https://api.example.com/v1/users/me
 * ```
 */

This approach means my getting started guide is versioned alongside my API. If I change how authentication works in version 2.0, the getting started guide for version 2.0 reflects those changes automatically.

Now, I want to share something personal. Early in my career, I treated documentation as an afterthought. I’d build the API, then grudgingly write some documentation at the end. The result was always the same: outdated, incomplete documentation that frustrated everyone who used it.

The turning point came when I started treating documentation as a first-class citizen in my development process. Now, I write documentation as I write code. I think about what developers will need to know, and I build that information into my code structure.

This approach has changed how I work. It’s made my APIs better because I’m forced to think about them from a user’s perspective. It’s reduced support requests because developers can find answers in the documentation. And it’s made maintenance easier because the documentation updates automatically when the code changes.

The tools I’ve shown you today are just that—tools. They’re not magic solutions. They require discipline and consistency to use effectively. But when used properly, they can transform your documentation from a burden into a valuable asset.

Remember, your documentation is the face of your API. It’s the first thing developers see and the thing they’ll judge you by. Make it good. Make it accurate. Make it helpful.

I’d love to hear about your experiences with API documentation. What challenges have you faced? What solutions have worked for you? Share your thoughts in the comments below—let’s learn from each other. And if you found this helpful, please share it with someone who might benefit from it.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: api documentation,typescript,swagger,developer experience,rest api



Similar Posts
Blog Image
NestJS GraphQL API: Complete Guide with Prisma and Redis Caching for Production

Learn to build high-performance GraphQL APIs using NestJS, Prisma ORM, and Redis caching. Master database optimization, authentication, and real-time subscriptions.

Blog Image
Build Multi-Tenant SaaS Apps with NestJS, Prisma and PostgreSQL Row-Level Security

Learn to build scalable multi-tenant SaaS apps with NestJS, Prisma & PostgreSQL RLS. Complete guide with authentication, tenant isolation & optimization tips.

Blog Image
Building Resilient Systems with Event-Driven Architecture and RabbitMQ

Learn how to decouple services using RabbitMQ and event-driven design to build scalable, fault-tolerant applications.

Blog Image
Building Production-Ready GraphQL API with TypeScript, Apollo Server, Prisma, and Redis

Learn to build a scalable GraphQL API with TypeScript, Apollo Server, Prisma, and Redis caching. Complete tutorial with authentication, real-time features & deployment.

Blog Image
Complete Guide: Integrating Next.js with Prisma ORM for Type-Safe Database Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe, full-stack applications. Build database-driven apps with seamless data management and enhanced developer experience.

Blog Image
Building Event-Driven Microservices with NestJS: RabbitMQ and MongoDB Complete Guide

Learn to build event-driven microservices with NestJS, RabbitMQ & MongoDB. Master async communication, error handling & monitoring for scalable systems.