Skip to main content

Command Palette

Search for a command to run...

OpenAPI Specification: Design APIs That Scale

Published
6 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

OpenAPI Specification: Design APIs That Scale

The API That Broke Everything (And How Documentation Could Have Saved Us)

Our undocumented API changes broke 50 client apps. Here's what we learned the hard way.

Table of Contents

  • API Development 2026
  • Design Principles
  • 5 Essential Patterns
  • Documentation Strategy
  • Testing Approach
  • Security Considerations
  • Monitoring
  • FAQ
  • Best Practices

API Development in 2026

APIs are the backbone of modern apps.

API-First Approach

# OpenAPI specification first
openapi: 3.1.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'

Design-First Benefits

// Generated TypeScript types
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: string;
}

// Auto-generated client
const users = await api.users.list();
// TypeScript knows the response type!

Business Impact

Good APIs = happy developers = more integrations.

Design Principles

Build APIs that last.

RESTful Conventions

// Standard HTTP methods
app.get('/api/users', listUsers);          // Read all
app.get('/api/users/:id', getUser);        // Read one
app.post('/api/users', createUser);        // Create
app.put('/api/users/:id', updateUser);     // Update (full)
app.patch('/api/users/:id', patchUser);    // Update (partial)
app.delete('/api/users/:id', deleteUser);  // Delete

Resource Naming

// ✅ Good: Plural nouns
GET /api/users
GET /api/orders/123/items

// ❌ Bad: Verbs or singular
GET /api/getUser
GET /api/user

Error Responses

// Consistent error format
interface APIError {
  error: {
    code: string;
    message: string;
    details?: any;
    requestId: string;
  };
}

app.use((err, req, res, next) => {
  res.status(err.status || 500).json({
    error: {
      code: err.code || 'INTERNAL_ERROR',
      message: err.message,
      requestId: req.id
    }
  });
});

Pattern 1: Request Validation

Schema Validation

// Validate with Zod
import { z } from 'zod';

const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().min(13).max(120).optional()
});

app.post('/api/users', async (req, res) => {
  try {
    const data = createUserSchema.parse(req.body);
    const user = await createUser(data);
    res.status(201).json(user);
  } catch (error) {
    if (error instanceof z.ZodError) {
      res.status(400).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid input',
          details: error.errors
        }
      });
    }
  }
});

Type Safety

Generate types from schema.

Pattern 2: Pagination

Cursor-Based

// Efficient pagination
interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    nextCursor?: string;
    prevCursor?: string;
    hasMore: boolean;
  };
}

app.get('/api/users', async (req, res) => {
  const { cursor, limit = 20 } = req.query;

  const users = await db.users
    .where('id', '>', cursor || '')
    .limit(limit + 1)
    .orderBy('id');

  const hasMore = users.length > limit;
  const data = hasMore ? users.slice(0, -1) : users;

  res.json({
    data,
    pagination: {
      nextCursor: hasMore ? data[data.length - 1].id : undefined,
      hasMore
    }
  });
});

Offset-Based

// Simple but can be slow
app.get('/api/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const offset = (page - 1) * limit;

  const [users, total] = await Promise.all([
    db.users.limit(limit).offset(offset),
    db.users.count()
  ]);

  res.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  });
});

Pattern 3: Authentication

JWT Implementation

// Secure JWT handling
import { SignJWT, jwtVerify } from 'jose';

const secret = new TextEncoder().encode(process.env.JWT_SECRET);

export async function createToken(userId: string) {
  return await new SignJWT({ sub: userId })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('2h')
    .sign(secret);
}

// Middleware
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({
      error: { code: 'UNAUTHORIZED', message: 'No token' }
    });
  }

  try {
    const { payload } = await jwtVerify(token, secret);
    req.userId = payload.sub;
    next();
  } catch {
    res.status(401).json({
      error: { code: 'INVALID_TOKEN', message: 'Token invalid' }
    });
  }
}

API Keys

// Simple API key auth
async function apiKeyAuth(req, res, next) {
  const apiKey = req.headers['x-api-key'];

  const key = await db.apiKeys.findOne({
    key: apiKey,
    active: true
  });

  if (!key) {
    return res.status(401).json({
      error: { code: 'INVALID_KEY' }
    });
  }

  // Track usage
  await db.apiKeys.update(key.id, {
    lastUsed: new Date(),
    usageCount: key.usageCount + 1
  });

  req.apiKey = key;
  next();
}

Pattern 4: Rate Limiting

Implementation

// Redis-based rate limiting
import { RateLimiterRedis } from 'rate-limiter-flexible';

const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 100, // requests
  duration: 60  // per minute
});

async function rateLimitMiddleware(req, res, next) {
  const key = req.apiKey?.id || req.ip;

  try {
    await rateLimiter.consume(key);

    // Add rate limit headers
    const remaining = await rateLimiter.get(key);
    res.setHeader('X-RateLimit-Limit', 100);
    res.setHeader('X-RateLimit-Remaining', remaining?.remainingPoints || 100);

    next();
  } catch {
    res.status(429).json({
      error: {
        code: 'RATE_LIMIT_EXCEEDED',
        message: 'Too many requests'
      }
    });
  }
}

Tiered Limits

Different limits per plan.

Pattern 5: Versioning

URL Versioning

// Version in path
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// v1Router
v1Router.get('/users', (req, res) => {
  // Old response format
  res.json({ users: [...] });
});

// v2Router
v2Router.get('/users', (req, res) => {
  // New response format with pagination
  res.json({
    data: [...],
    pagination: {...}
  });
});

Header Versioning

// Accept-Version header
app.use((req, res, next) => {
  const version = req.headers['accept-version'] || '1';
  req.apiVersion = version;
  next();
});

Documentation Strategy

Interactive Docs

// Swagger UI setup
import swaggerUi from 'swagger-ui-express';
import { readFileSync } from 'fs';
import { load } from 'js-yaml';

const spec = load(readFileSync('./openapi.yaml', 'utf8'));

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(spec));

Code Examples

## Create User

POST /api/users

### Request
```json
{
  "email": "user@example.com",
  "name": "John Doe"
}

Response

{
  "id": "usr_123",
  "email": "user@example.com",
  "name": "John Doe",
  "createdAt": "2026-02-12T10:00:00Z"
}

## Testing Approach

### Integration Tests

```typescript
// Test API endpoints
import { describe, it, expect } from 'vitest';

describe('Users API', () => {
  it('creates user', async () => {
    const response = await fetch('http://localhost:3000/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test@example.com',
        name: 'Test User'
      })
    });

    expect(response.status).toBe(201);
    const user = await response.json();
    expect(user.email).toBe('test@example.com');
  });
});

Contract Testing

Ensure API matches spec.

Performance

EndpointAvg Responsep95p99
GET /users50ms80ms120ms
POST /users100ms150ms200ms
GET /users/:id20ms30ms50ms

FAQ

Q1: REST vs GraphQL?

REST for public APIs, GraphQL for frontend-driven apps.

Q2: How to version APIs?

URL versioning is clearest for clients.

Q3: Rate limiting strategy?

Per-user or per-API-key with tiered limits.

Q4: Documentation tool?

OpenAPI + Swagger UI or ReadMe.io.

Q5: API key vs OAuth?

API keys for server-to-server, OAuth for user apps.

Best Practices

Checklist

  • [ ] OpenAPI spec written
  • [ ] Input validation
  • [ ] Error handling
  • [ ] Authentication
  • [ ] Rate limiting
  • [ ] Documentation
  • [ ] Tests written
  • [ ] Monitoring setup

Conclusion

Great APIs drive adoption.

Key takeaways:

  • Design first
  • Document thoroughly
  • Validate inputs
  • Version carefully
  • Monitor constantly

Build APIs developers love.

Resources:

  • OpenAPI Specification
  • API Design Patterns
  • Testing Strategies
  • Security Guidelines

Next Steps:

  1. Write OpenAPI spec
  2. Implement validation
  3. Add authentication
  4. Generate documentation
  5. Deploy with monitoring

Build better APIs today.