Part 4: API Testing and Contract Testing

Introduction

API testing ensures your HTTP endpoints behave correctly—proper status codes, valid responses, correct error handling, and consistent contracts. In my microservices architecture, APIs are the integration points between services, making API testing critical for system reliability.

This part covers practical API testing techniques I use in production TypeScript microservices, including REST API testing, authentication, and contract testing between services.

REST API Testing Fundamentals

Example: User Management API

user-controller.ts:

// src/api/user-controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user-service';
import { z } from 'zod';

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  password: z.string().min(8),
});

export class UserController {
  constructor(private readonly userService: UserService) {}

  async createUser(req: Request, res: Response, next: NextFunction) {
    try {
      const validation = CreateUserSchema.safeParse(req.body);
      
      if (!validation.success) {
        return res.status(400).json({
          error: 'Validation failed',
          details: validation.error.errors,
        });
      }

      const user = await this.userService.createUser(validation.data);
      
      return res.status(201).json({
        id: user.id,
        email: user.email,
        name: user.name,
        createdAt: user.createdAt,
      });
    } catch (error) {
      if (error instanceof Error) {
        if (error.message.includes('already exists')) {
          return res.status(409).json({ error: error.message });
        }
      }
      next(error);
    }
  }

  async getUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params;
      const user = await this.userService.getUserById(id);
      
      return res.status(200).json({
        id: user.id,
        email: user.email,
        name: user.name,
        createdAt: user.createdAt,
      });
    } catch (error) {
      if (error instanceof Error && error.message.includes('not found')) {
        return res.status(404).json({ error: error.message });
      }
      next(error);
    }
  }

  async updateUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params;
      const { name } = req.body;
      
      const user = await this.userService.updateUser(id, { name });
      
      return res.status(200).json({
        id: user.id,
        email: user.email,
        name: user.name,
        updatedAt: user.updatedAt,
      });
    } catch (error) {
      next(error);
    }
  }

  async deleteUser(req: Request, res: Response, next: NextFunction) {
    try {
      const { id } = req.params;
      await this.userService.deleteUser(id);
      
      return res.status(204).send();
    } catch (error) {
      next(error);
    }
  }
}

Setting Up Test Server

test-server.ts:

API Tests with Supertest

Installation:

user-api.test.ts:

Testing Authentication

JWT Authentication Middleware

auth-middleware.ts:

Testing authentication:

OpenAPI/Swagger Validation

Testing that your API matches its OpenAPI specification.

Installation:

openapi.yaml:

OpenAPI validation tests:

Contract Testing with Pact

Contract testing ensures services agree on their API contracts.

Installation:

Consumer Side (Order Service)

order-service-consumer.pact.test.ts:

Provider Side (User Service)

user-service-provider.pact.test.ts:

Testing Rate Limiting

rate-limiter.ts:

Testing rate limiting:

API Versioning Tests

v1 and v2 controllers:

Version tests:

Best Practices

1. Test HTTP Status Codes

Always verify correct status codes:

  • 200 - Success

  • 201 - Created

  • 204 - No Content

  • 400 - Bad Request

  • 401 - Unauthorized

  • 403 - Forbidden

  • 404 - Not Found

  • 409 - Conflict

  • 422 - Unprocessable Entity

  • 429 - Too Many Requests

  • 500 - Internal Server Error

2. Test Content-Type Headers

3. Test Error Responses

4. Test CORS Headers

Key Takeaways

  1. Test API contracts thoroughly - Status codes, headers, response bodies

  2. Use supertest for HTTP testing - Simple and effective

  3. Validate against OpenAPI specs - Ensure documentation matches implementation

  4. Contract testing prevents integration issues - Services agree on interfaces

  5. Test authentication and authorization - Security is critical

  6. Test rate limiting - Protect your APIs

  7. Version your APIs - Test all supported versions

What's Next?

In Part 5: End-to-End Testing, we'll cover:

  • Browser automation with Playwright

  • Testing complete user workflows

  • Visual regression testing

  • E2E test patterns and best practices


This article is part of the Software Testing 101 series.

Last updated