← Back to all projects

API DESIGN VERSIONING MASTERY

In the modern software landscape, an API is a **contract**. Once a developer integrates with your API, you cannot simply change the shape of your data without breaking their business. API Design is the difference between a system that scales gracefully and one that collapses under the weight of its own technical debt.

Learn API Design & Versioning: From Zero to API Architect

Goal: Deeply understand the art and science of API Design and Versioning—mastering the ability to build robust, scalable, and evolvable interfaces. You will learn how to write perfect OpenAPI specifications, implement sophisticated versioning strategies (URI, Header, and Media Type), and apply design patterns to maintain backward compatibility without bloating your codebase.


Why API Design & Versioning Matters

In the modern software landscape, an API is a contract. Once a developer integrates with your API, you cannot simply change the shape of your data without breaking their business. API Design is the difference between a system that scales gracefully and one that collapses under the weight of its own technical debt.

The Business Case for Proper API Design

Breaking changes have real costs:

  • According to Postman’s 2024 report, 52% of teams cite breaking changes as their main migration issue
  • 68% of enterprises cite versioning as a top challenge in API lifecycle management
  • Companies with clear versioning mechanisms can reduce breaking changes by 50%
  • APIs with proper versioning experience 40% fewer security issues during transitions
  • 36% of companies spend more time troubleshooting APIs than developing new features

Real-World Impact

Poor API Design                      Well-Designed API
┌──────────────────┐                ┌──────────────────┐
│ Breaking Change  │                │ Versioned Update │
│ Released         │                │ Released         │
└────────┬─────────┘                └────────┬─────────┘
         │                                   │
         v                                   v
┌──────────────────┐                ┌──────────────────┐
│ Mobile Apps      │                │ Old Clients:     │
│ CRASH            │                │ Still working    │
│                  │                │                  │
│ Support Tickets  │                │ New Clients:     │
│ Flood In         │                │ New features     │
│                  │                │                  │
│ Customer Churn   │                │ Seamless DX      │
└──────────────────┘                └──────────────────┘

Why This Matters More Than Ever

  • Developer Experience (DX): A well-designed API is intuitive, self-documenting, and predictable. Over 80% of organizations now use APIs as a primary integration method.
  • Business Continuity: Breaking changes cost money, time, and trust. Unexpected API changes lead to customer churn and support overhead.
  • The “Stripe” Standard: Companies like Stripe have set the bar high, supporting dozens of versions simultaneously for years using date-based versioning.
  • Scalability: Proper design allows you to evolve the backend implementation independently of the public interface.
  • Security: Poor versioning practices increase vulnerabilities during transitions.

Historical Context

REST APIs evolved from SOAP (2000s) and RPC patterns. Roy Fielding’s 2000 dissertation defined REST principles, but it took until the 2010s for OpenAPI (formerly Swagger) to standardize API contracts. Today, OpenAPI 3.1 is the dominant standard, with GraphQL and AsyncAPI emerging as complementary approaches for specific use cases.

The versioning problem became acute around 2015 when mobile apps made it impossible to force-upgrade all clients simultaneously. Stripe’s 2017 blog post on API versioning revolutionized how the industry thinks about backward compatibility.


Prerequisites & Background Knowledge

Essential Prerequisites (Must Have)

Before starting these projects, you should have:

  1. HTTP Fundamentals
    • Understanding of GET, POST, PUT, PATCH, DELETE methods
    • Knowledge of status codes (200, 201, 400, 404, 500)
    • Headers and request/response structure
  2. JSON Data Structures
    • Object and array syntax
    • Nested structures and references
  3. Backend Programming
    • Proficiency in at least one language (JavaScript, Python, Go, Java)
    • Understanding of web frameworks (Express, FastAPI, Flask, Spring)
  4. Basic Git & Command Line
    • Creating branches and commits
    • Running shell commands

Helpful But Not Required

These topics will be learned during the projects:

  • OpenAPI/Swagger specification syntax
  • Design patterns (Strategy, Adapter)
  • Database migrations and schema evolution
  • CI/CD pipelines and automation
  • Distributed systems concepts

Self-Assessment Questions

Can you answer these? If not, study the referenced topics first:

  • HTTP: What’s the difference between PUT and PATCH? When would each cause a 409 Conflict?
  • REST: Why shouldn’t you use verbs in URL paths like /getUser?
  • JSON: How do you represent a one-to-many relationship in JSON?
  • Programming: Can you write a middleware function that intercepts requests?
  • Semantics: What does “idempotent” mean in the context of API operations?

Development Environment Setup

Required Tools:

  • Code Editor: VS Code with OpenAPI extensions (recommended: Swagger Viewer)
  • API Testing: Postman or curl (both free)
  • Language Runtime: Node.js 18+, Python 3.10+, or Go 1.21+ (depending on project choice)

Recommended Tools:

  • Swagger Editor: editor.swagger.io for live spec validation
  • Prism: For mock servers (npm install -g @stoplight/prism-cli)
  • Spectral: For API linting (npm install -g @stoplight/spectral-cli)
  • Database: PostgreSQL or SQLite for Projects 5, 8
  • Docker: For Projects 5, 11, 12

Time Investment

Realistic estimates for complete mastery:

  • Beginner projects (1-2): 2-3 weekends (16-24 hours total)
  • Intermediate projects (3-6, 9-10): 4-8 weeks part-time (40-80 hours)
  • Advanced projects (4, 7, 11): 6-12 weeks part-time (60-120 hours)
  • Expert projects (8, 12): 3-6 months part-time (100-200 hours)

Total learning path: 6-12 months to internalize all concepts and complete all projects.

Important Reality Check

API design is deceptively difficult. The syntax is simple (YAML/JSON), but the decisions are hard:

  • Should this field be optional or required?
  • Is this change backward compatible?
  • How do I evolve this without breaking clients?

Expect to:

  • Redesign your specs multiple times before finding the “right” model
  • Struggle with the tradeoff between “clean design” and “backward compatibility”
  • Spend more time thinking than coding (this is good!)
  • Question every decision (also good!)

Concept Summary Table

Concept Cluster What You Need to Internalize
Resource Mapping APIs are about nouns and state, not verbs and actions. Mapping real-world entities to URLs.
Contract-First Design The specification (OpenAPI) is the source of truth, not the code. Design before building.
Semantic Versioning Major (breaking), Minor (feature), Patch (fix). Understanding what constitutes a “break.”
Idempotency Ensuring that calling an operation multiple times has the same result as calling it once.
Versioning Strategies URI vs. Header vs. Media Type. Trade-offs in caching, visibility, and complexity.
Strategy Pattern Decoupling the API contract from the business logic to support multiple versions concurrently.

Deep Dive Reading by Concept

This section maps each concept to specific book chapters. Read these before or alongside the projects to build strong mental models.

API Foundations & Design

Concept Book & Chapter
The API Lifecycle “Principles of Web API Design” by James Higginbotham — Ch. 1-2
Resource-Oriented Design “The Design of Web APIs” by Arnaud Lauret — Ch. 4: “Designing Resources”
OpenAPI Fundamentals “The Design of Web APIs” by Arnaud Lauret — Ch. 12: “Documenting the API”

Versioning & Evolution

Concept Book & Chapter
Versioning Strategies “API Design Patterns” by JJ Geewax — Ch. 22: “Versioning”
Breaking Changes “Designing Web APIs” by Jin, Sahni, & Shevat — Ch. 9: “Evolving an API”
Backward Compatibility “Principles of Web API Design” by James Higginbotham — Ch. 11: “Evolving APIs”

Essential Reading Order

  1. Foundation (Week 1):
    • Principles of Web API Design Ch. 1-3 (Aligning with goals)
    • The Design of Web APIs Ch. 3-4 (Resource design)
  2. The Contract (Week 2):
    • OpenAPI 3.1 Specification (Official Docs - read the structure)
    • The Design of Web APIs Ch. 12
  3. Evolution (Week 3):
    • API Design Patterns Ch. 22
    • Designing Web APIs Ch. 9

Quick Start Guide

Feeling overwhelmed by 12 projects? Start here.

Your First 48 Hours

Day 1: Foundation (4 hours)

  1. Read Arnaud Lauret’s “The Design of Web APIs” Ch. 3-4 (Resource design)
  2. Explore 3 real-world API docs: Stripe, GitHub, Twilio
  3. Compare their versioning approaches

Day 2: Your First Spec (4 hours)

  1. Start Project 1 (API Spec Architect)
  2. Model a simple “Blog API” with posts and comments
  3. Validate it in Swagger Editor
  4. Generate docs with Redocly

First Week Goals

By end of week 1, you should:

  • Have completed Project 1 (OpenAPI spec)
  • Understand what makes a change “breaking”
  • Know the difference between URI and Header versioning
  • Be able to explain idempotency

First Month Milestones

  • Week 1: Projects 1-2 (Spec + Mock Server)
  • Week 2: Project 3 (URI Versioning)
  • Week 3: Project 4 (Strategy Pattern)
  • Week 4: Review and solidify concepts

Success metric: You can design a spec and explain your design decisions confidently.


Path 1: The Complete Beginner

Starting from zero REST knowledge

Week 1-2:  Project 1 (Spec Architect)
           ↓
Week 3:    Project 2 (Mock Server)
           ↓
Week 4-5:  Project 3 (URI Versioning)
           ↓
Week 6:    Pause - Read JJ Geewax Ch. 22
           ↓
Week 7-9:  Project 4 (Strategy Pattern)
           ↓
Week 10:   Project 6 (Deprecation)

Expected outcome: Solid foundation in API design and basic versioning.

Path 2: The Backend Developer

You’ve built APIs before, want to level up

Week 1:    Project 1 (Quick review)
           ↓
Week 2-3:  Project 4 (Strategy Pattern) ← Start here
           ↓
Week 4-5:  Project 7 (HATEOAS)
           ↓
Week 6-8:  Project 11 (Stripe-Style) ← The masterpiece
           ↓
Week 9:    Project 12 (Gateway)

Expected outcome: Advanced architectural patterns, production-ready versioning.

Path 3: The DevOps/Platform Engineer

Focus on automation and infrastructure

Week 1:    Project 2 (Linting/Mocking)
           ↓
Week 2-3:  Project 9 (Diffing Engine) ← Your bread and butter
           ↓
Week 4-5:  Project 12 (Gateway Router)
           ↓
Week 6-7:  Project 5 (Legacy Adapter)

Expected outcome: CI/CD mastery, automated API governance.

Path 4: The Architect

System design and long-term evolution

Week 1-2:  Projects 1-2 (Fast review)
           ↓
Week 3-4:  Project 8 (Schema Evolution) ← Critical
           ↓
Week 5-7:  Project 11 (Stripe-Style)
           ↓
Week 8-10: Final Project (Immortal API)

Expected outcome: Ability to design APIs that last 10+ years.


Project 1: The API Spec Architect (Contract-First Mastery)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: YAML (OpenAPI 3.1)
  • Alternative Programming Languages: JSON, TypeScript (TypeSpec)
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 1: Beginner
  • Knowledge Area: API Specification / Domain Modeling
  • Software or Tool: Swagger Editor, Stoplight Elements
  • Main Book: “The Design of Web APIs” by Arnaud Lauret

What you’ll build: A comprehensive OpenAPI 3.1 specification for a complex “Digital Wallet” system, including accounts, transactions, currency conversions, and user profiles.

Why it teaches API Design: You will learn to model complex domain relationships using only a specification. This forces you to think about data structures, security schemes, and error codes before a single line of application code is written.

Core challenges you’ll face:

  • Modeling Recursive Relationships → maps to understanding how accounts link to transactions
  • Implementing Polymorphism with oneOf/anyOf → maps to different transaction types (Credit, Debit, Transfer)
  • Designing Reusable Components → maps to minimizing duplication in the specification

Key Concepts:

  • OpenAPI Components: Swagger.io Documentation
  • API Status Codes: MDN Web Docs - HTTP Status Codes
  • Resource Hierarchy: “The Design of Web APIs” Ch. 4

Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic understanding of YAML and HTTP.


Real World Outcome

You will have a single openapi.yaml file that, when dropped into a viewer like Swagger UI, renders a professional, interactive documentation portal.

Example Output:

# Rendering your spec with Redocly
$ npx @redocly/cli build-docs openapi.yaml
[info] Successfully generated docs into redoc-static.html

The Core Question You’re Answering

“Can I describe my entire system’s behavior perfectly without writing a single line of executable code?”

Before you write any code, sit with this question. A spec is a contract. If the spec is vague, the implementation will be buggy. Developers often rush to code and “document later,” but later never comes.


Concepts You Must Understand First

Stop and research these before coding:

  1. REST Resource Modeling
    • What is the difference between a resource and an endpoint?
    • Why shouldn’t you use verbs in URLs?
    • Book Reference: “The Design of Web APIs” Ch. 4 - Arnaud Lauret
  2. OpenAPI 3.x Structure
    • What is the purpose of the components section?
    • How does $ref work for reusability?
    • Book Reference: “The Design of Web APIs” Ch. 12

Questions to Guide Your Design

Before implementing, think through these:

  1. Entity Relationships
    • How should a “Transaction” link to a “User”? By ID or by full object?
    • Should GET /accounts return the full transaction history or just a link?
  2. Security
    • How will you describe Bearer Token authentication in the spec?
    • Can you define different scopes for reading vs writing?

Thinking Exercise

The “New Field” Impact

Imagine you have a User object. You need to add a middleName field.

# Current
User:
  type: object
  properties:
    firstName: { type: string }
    lastName: { type: string }

Questions while analyzing:

  • Is adding middleName a breaking change?
  • What if you make it required: [firstName, middleName, lastName]?
  • How would a mobile app built 6 months ago react to this change?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the difference between PUT and PATCH and when to use each.”
  2. “What makes a change ‘breaking’ in an API?”
  3. “How do you handle pagination in a RESTful way?”
  4. “Why is the OpenAPI spec valuable for frontend teams?”
  5. “Describe the importance of idempotency in API design.”

Hints in Layers

Hint 1: Start with the paths List all the actions a user can take (create account, transfer money) and map them to HTTP methods.

Hint 2: Identify common objects Create a schemas section in components for User, Account, and Transaction so you can reuse them.

Hint 3: Use meaningful status codes Don’t just return 200. Use 201 for creation, 400 for validation errors, and 404 for missing resources.

Hint 4: Validate your YAML Use an editor like Swagger Editor (editor.swagger.io) to catch syntax errors immediately.


Books That Will Help

Topic Book Chapter
Resource Modeling “The Design of Web APIs” by Arnaud Lauret Ch. 4
Documentation “The Design of Web APIs” by Arnaud Lauret Ch. 12

Common Pitfalls & Debugging

Problem 1: “Swagger Editor shows ‘Resolver error at paths…’“

  • Why: You likely have a typo in a $ref path or the referenced component doesn’t exist
  • Fix: Check that $ref: '#/components/schemas/User' matches exactly the name in your components.schemas section (case-sensitive!)
  • Quick test: Copy the reference path and manually navigate to it in your YAML to verify it exists

Problem 2: “All my endpoints return the same schema but I have tons of duplication”

  • Why: You’re not using $ref for reusability
  • Fix: Define common responses in components.responses and reference them: $ref: '#/components/responses/NotFound'
  • Quick test: If you’re copy-pasting YAML, you need more $ref usage

Problem 3: “Should ‘created_at’ be required or optional?”

  • Why: This is a design decision, not a syntax error
  • Fix: Server-generated fields (timestamps, IDs) should be marked readOnly: true and excluded from POST requests but required in GET responses
  • Quick test: Ask “Does the client provide this, or does the server generate it?”

Problem 4: “Swagger UI shows my example, but it doesn’t match my schema”

  • Why: Examples are not validated by default
  • Fix: Use Spectral linter with oas-example-schema-matches rule to catch mismatches
  • Quick test: spectral lint openapi.yaml --ruleset spectral:oas

Problem 5: “I changed my spec but the mock server still returns old data”

  • Why: Prism caches examples or the process didn’t restart
  • Fix: Kill and restart Prism: pkill prism && prism mock openapi.yaml
  • Quick test: Add ?__cacheBust=123 to your request URL

Project 2: The Contract Validator & Mock Server

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Node.js
  • Alternative Programming Languages: Python, Go
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Tooling / Contract Testing
  • Software or Tool: Prism, Spectral
  • Main Book: “API Design Patterns” by JJ Geewax

What you’ll build: A toolchain that takes your Project 1 spec, runs a “linter” to ensure best practices (naming, status codes), and spins up a dynamic mock server that validates incoming requests against the spec.

Why it teaches API Design: This project makes the “Contract” alive. You’ll see how a spec can actually enforce behavior and provide a working sandbox for frontend teams before the backend is even built.

Core challenges you’ll face:

  • Setting up Automated Linting → maps to enforcing consistent naming conventions
  • Dynamic Response Generation → maps to using examples from the spec to drive the mock
  • Request Validation → maps to intercepting calls and checking them against OAS schemas

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 1 completed, basic CLI tool knowledge.


Real World Outcome

A CI/CD pipeline step that fails if the API design is “ugly” and a local command that developers run to start a “fake” backend.

Example Output:

$ spectral lint openapi.yaml
✖  Path must follow kebab-case  paths./user_profile
✖  Operation must have a 400 response  paths./transfer.post

$ prism mock openapi.yaml
[PRISM] [14:00:00] Mock server listening on http://127.0.0.1:4010
[PRISM] [14:00:05] GET /accounts/123 -> 200 OK (Validated)
[PRISM] [14:00:12] POST /transfer {"amount": 100} -> 201 Created

# Frontend developers can now work in parallel!

The Core Question You’re Answering

“Can I catch API design mistakes before they reach production?”

Before you code, consider this: Every badly-named endpoint, every missing 400 response, every inconsistent casing becomes technical debt. Linters automate what code reviews miss. Mock servers let frontend teams work independently, doubling team productivity.


Concepts You Must Understand First

Stop and research these before coding:

  1. API Linting and Style Guides
    • What makes an API “RESTful”? (Hint: More than just using HTTP)
    • Why does consistent naming matter across 100+ endpoints?
    • Book Reference: “The Design of Web APIs” Ch. 10 - Arnaud Lauret
  2. Contract-Driven Development
    • How does a mock server validate requests against a schema?
    • What happens if you send invalid JSON to a mock?
    • Book Reference: “API Design Patterns” Ch. 1 - JJ Geewax

Questions to Guide Your Design

Before implementing, think through these:

  1. Linting Rules
    • Should you enforce kebab-case or snake_case in paths?
    • Do all POST endpoints need to return 201? What about idempotent creates?
  2. Mock Server Behavior
    • Should the mock server return static examples or generate random data?
    • How should it handle path parameters like /users/{id}?

Thinking Exercise

The “Validation Paradox”

You have this spec:

/users/{id}:
  get:
    responses:
      '200':
        schema:
          type: object
          required: [id, name]
      '404':
        description: User not found

Someone requests GET /users/abc (non-numeric ID).

Questions while analyzing:

  • Should the mock return 404 (semantically correct) or 400 (validation error)?
  • Does your spec even define a 400 response?
  • How would Prism handle this vs. a real server?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What are the benefits of contract-first API development?”
  2. “How would you enforce API design standards across a team of 20 developers?”
  3. “Explain how a mock server can speed up frontend development.”
  4. “What’s the difference between validation at the gateway vs. validation in the service?”
  5. “How do you handle API versioning in your OpenAPI specs?”

Hints in Layers

Hint 1: Start with Spectral Create a .spectral.yaml file with rules like path-case, operation-tag-defined, operation-success-response.

Hint 2: Use Prism’s validation mode Run prism mock -d openapi.yaml to enable request/response validation. It will return errors if clients send invalid data.

Hint 3: Integrate into CI Add a GitHub Action or GitLab CI step: spectral lint openapi.yaml || exit 1 to block merges with bad specs.

Hint 4: Generate examples automatically Use a tool like openapi-examples-validator to ensure your example fields match your schema definitions.


Books That Will Help

Topic Book Chapter
Contract Testing “API Design Patterns” by JJ Geewax Ch. 1
API Style Guides “The Design of Web APIs” by Arnaud Lauret Ch. 10

Common Pitfalls & Debugging

Problem 1: “Spectral says my paths are wrong but they look fine”

  • Why: You’re using inconsistent casing (e.g., /userProfile vs /user-profile)
  • Fix: Choose a convention (kebab-case recommended) and apply it everywhere
  • Quick test: Run spectral lint with the path-case rule

Problem 2: “Prism returns 500 errors for everything”

  • Why: Your spec has invalid references or malformed examples
  • Fix: Validate spec first with swagger-cli validate openapi.yaml
  • Quick test: Load spec in Swagger Editor and fix all red errors

Problem 3: “Frontend team says mock returns wrong data types”

  • Why: Your examples don’t match your schema (string vs number, etc.)
  • Fix: Use Spectral’s oas-example-schema-matches rule
  • Quick test: Compare your example against your type definition

Project 3: The Multi-Version Router (URI Versioning)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: TypeScript (Node/Express or Hono)
  • Alternative Programming Languages: Go (Chi), Python (FastAPI)
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Implementation / Routing
  • Software or Tool: Express.js, Postman
  • Main Book: “API Versioning for The Real World” by Dan Patrascu

What you’ll build: A backend service that serves two versions of the same resource simultaneously using URI versioning (/v1/... and /v2/...). You will implement a change where a field is renamed (e.g., user_name to fullName) and both versions must still work.

Why it teaches Versioning: You’ll grapple with code duplication vs. abstraction. Should you have two controllers? Or one controller with an if statement? This project teaches you the cost of branching.

Core challenges you’ll face:

  • Namespace Routing → maps to organizing code so v1 and v2 don’t collide
  • Shared Data Layer → maps to making one database model serve two different API shapes
  • Testing for Regressions → maps to ensuring changes in v2 don’t accidentally break v1

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Project 1 & 2.


Real World Outcome

Two distinct endpoints that return the same underlying data but in different formats.

Example Output:

$ curl http://api.local/v1/users/1
{ "id": 1, "user_name": "Alice" }

$ curl http://api.local/v2/users/1
{ "id": 1, "fullName": "Alice Smith" }

# Same database record, two different API shapes!

The Core Question You’re Answering

“How do I support old mobile apps while shipping new features?”

Before coding, grasp this reality: Mobile apps don’t auto-update. Users on iOS 14 from 2020 might still use your app in 2025. URI versioning (/v1/, /v2/) is the most visible, cacheable approach—but it creates code duplication. This project forces you to confront that tradeoff.


Concepts You Must Understand First

Stop and research these before coding:

  1. URI Versioning vs. Other Strategies
    • Why is /v1/users better than /users?version=1?
    • How does this affect HTTP caching (CDN, browser cache)?
    • Book Reference: “API Design Patterns” Ch. 22 - JJ Geewax
  2. Code Organization Patterns
    • Should v1 and v2 share the same controller logic?
    • How do you avoid duplicating database queries?
    • Book Reference: “Clean Architecture” Ch. 22 - Robert C. Martin

Questions to Guide Your Design

Before implementing, think through these:

  1. Routing Strategy
    • Do you create separate Express routers for /v1/ and /v2/?
    • Should version detection happen in middleware or per-route?
  2. Data Transformation
    • Do you transform at the database layer or presentation layer?
    • If user_name in DB becomes fullName in v2, where does the mapping happen?

Thinking Exercise

The “Shared Logic Dilemma”

You have business logic that validates user input (e.g., “email must be valid format”).

// Option A: Duplicate the validation in v1 and v2
// v1/users.controller.ts
if (!isValidEmail(email)) throw new Error();

// v2/users.controller.ts
if (!isValidEmail(email)) throw new Error();

// Option B: Shared service layer
// shared/users.service.ts
validateEmail(email) { ... }

Questions while analyzing:

  • If you fix a bug in validation logic, do you want it applied to both versions automatically?
  • What if v2 has stricter validation (e.g., email + phone required)?
  • Where do you draw the line between “shared” and “version-specific”?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What are the pros and cons of URI versioning compared to header-based versioning?”
  2. “How would you handle a breaking database schema change in a versioned API?”
  3. “Explain how you would deprecate v1 while keeping it functional for legacy clients.”
  4. “How does URI versioning affect HTTP caching?”
  5. “Can you describe a scenario where URI versioning is the wrong choice?”

Hints in Layers

Hint 1: Start with routing Create two routers: v1Router and v2Router. Mount them at /api/v1 and /api/v2 in your main app.

Hint 2: Share the database layer Both versions should call the same repository/DAO. Version-specific transformations happen in controllers or response mappers.

Hint 3: Use DTOs (Data Transfer Objects) Create UserV1DTO and UserV2DTO classes. Map from your database model to the appropriate DTO based on the route.

Hint 4: Write version-aware tests Don’t just test “does /users work?” Test “does /v1/users return user_name and /v2/users return fullName?”


Books That Will Help

Topic Book Chapter
Versioning Strategies “API Design Patterns” by JJ Geewax Ch. 22
Clean Architecture “Clean Architecture” by Robert C. Martin Ch. 22

Common Pitfalls & Debugging

Problem 1: “Changes to v2 accidentally break v1”

  • Why: You’re sharing too much code without proper abstraction
  • Fix: Create separate controllers or use transformation layers that are tested independently
  • Quick test: Make a change to v2, then run v1 tests. If they fail, you have coupling.

Problem 2: “URL /v1/v1/users appears in responses”

  • Why: Your base URL configuration is duplicating the version prefix
  • Fix: Check your route mounting. Use app.use('/v1', v1Router), not app.use('/api/v1', v1Router) if your router already includes /v1/
  • Quick test: console.log(req.baseUrl) to see what Express thinks the base is

Problem 3: “Database query runs twice for the same request”

  • Why: Both your v1 and v2 controllers are fetching independently
  • Fix: Extract data fetching to a shared service, let version-specific logic only handle transformation
  • Quick test: Add logging to your database queries and check if the same query runs multiple times

Problem 4: “Not sure whether to version /health or /metrics endpoints”

  • Why: Operational endpoints are different from business endpoints
  • Fix: Keep operational endpoints unversioned (/health) since they’re for infrastructure, not clients
  • Quick test: Ask “Would a monitoring system care about API versions?” If no, don’t version it.

Project 4: The Strategy Pattern Implementer (Header Versioning)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: TypeScript/JavaScript
  • Alternative Programming Languages: C#, Java, Go
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Software Design Patterns
  • Software or Tool: Any modern Web Framework
  • Main Book: “API Design Patterns” by JJ Geewax

What you’ll build: A version-aware API where the version is NOT in the URL, but in the Accept-Version custom header. You will use the Strategy Pattern to select the appropriate “Transformer” class that shapes the response based on the requested version.

Why it teaches Versioning: This project forces you to decouple your routing logic from your business logic. You’ll learn how to keep your controllers “version-agnostic” while using specialized transformers to handle the API contract variations.

Core challenges you’ll face:

  • Header Parsing & Defaults → maps to deciding what version to serve if no header is present
  • The Strategy Registry → maps to dynamically loading the right transformer for v1, v2, etc.
  • Avoiding Code Duplication → maps to using inheritance or composition in your transformers

Key Concepts:

  • Strategy Pattern: Refactoring.Guru - Strategy Pattern
  • Content Negotiation: MDN - Content Negotiation
  • Custom Headers: IETF RFC 6648 (Deprecating X- prefixes)

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Understanding of Object-Oriented Design Patterns.


Real World Outcome

A single endpoint that behaves differently based on the client’s headers.

Example Output:

# Requesting v1
$ curl -H "Accept-Version: 1.0" http://api.local/profile
{ "name": "John Doe", "address": "123 Main St" }

# Requesting v2
$ curl -H "Accept-Version: 2.0" http://api.local/profile
{ "firstName": "John", "lastName": "Doe", "location": { "street": "123 Main St" } }

The Core Question You’re Answering

“How can I support multiple API versions without my controllers becoming a mess of if/else statements?”

Before coding, imagine supporting 10 versions. If you have 10 if statements in every function, your code is unmaintainable. The Strategy Pattern allows you to swap “transformers” based on the request metadata.


Concepts You Must Understand First

Stop and research these before coding:

  1. The Strategy Design Pattern
    • How does a Context object use a Strategy interface?
    • How can you implement this without a heavy OOP language? (e.g., using a map of functions)
    • Book Reference: “Design Patterns” - Gamma et al. (The Gang of Four)
  2. HTTP Content Negotiation
    • What are the Accept and Content-Type headers?
    • How does a server decide which version to return when multiple are supported?
    • Book Reference: “API Design Patterns” Ch. 22

Questions to Guide Your Design

Before implementing, think through these:

  1. Version Resolution
    • Where should the version selection happen? In a middleware, or inside the controller?
    • What happens if the client requests a version that doesn’t exist? (Return 406 Not Acceptable or 400 Bad Request?)
  2. Data Transformation
    • Should the “v1 strategy” fetch from a different database table, or just hide fields from the main model?
    • How do you handle common logic that both v1 and v2 share?

Thinking Exercise

The “Branching” Nightmare

Compare these two pseudo-code snippets:

Approach A (Conditional logic):

function getProfile(req, res) {
  const data = db.getUser();
  if (req.header('v') === '1') {
    return res.json({ name: data.name });
  } else {
    return res.json({ first: data.first, last: data.last });
  }
}

Approach B (Strategy Pattern):

function getProfile(req, res) {
  const data = db.getUser();
  const transformer = TransformerFactory.get(req.header('v'));
  return res.json(transformer.transform(data));
}

Questions while analyzing:

  • Which one is easier to test?
  • Which one is easier to delete when v1 is deprecated?
  • How many lines of code would you have to change in Approach A if you added v3?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What are the pros and cons of URI versioning vs Header versioning?”
  2. “How would you use the Strategy Pattern to manage API versioning?”
  3. “What HTTP status code should you return for an unsupported version?”
  4. “How do you handle default versions for existing clients?”
  5. “Can you explain how Stripe handles versioning without breaking old clients?”

Hints in Layers

Hint 1: Create a Version Middleware Extract the version from the header and attach it to the request object (req.apiVersion).

Hint 2: Define a Transformer Interface Create a base class or interface with a transform(data) method. Every version (v1, v2) will implement this.

Hint 3: Use a Factory Create a TransformerFactory that takes a version string and returns the correct instance of a transformer.

Hint 4: Centralize your defaults If no version is provided, always default to the oldest stable version to avoid breaking unknown clients, or the latest version for new ones (choose a policy).


Books That Will Help

Topic Book Chapter
Strategy Pattern “Design Patterns” by Gamma et al. Ch. 5 (Behavioral)
Versioning Patterns “API Design Patterns” by JJ Geewax Ch. 22

Common Pitfalls & Debugging

Problem 1: “Version header is ignored; always getting default version”

  • Why: Middleware might not be extracting the header correctly
  • Fix: Check exact header name (case-sensitive!): Accept-Version vs accept-version
  • Quick test: console.log(req.headers) to see all incoming headers

Problem 2: “Strategy Factory returns undefined transformer”

  • Why: Version string doesn’t match registered keys (“1.0” vs “1” vs “v1”)
  • Fix: Normalize version strings in middleware before lookup
  • Quick test: Add logging in factory: console.log('Requested:', version, 'Available:', Object.keys(strategies))

Problem 3: “Both v1 and v2 transformers run on same request”

  • Why: You’re not returning early after transformation
  • Fix: Ensure strategy selection is mutually exclusive with if/else or switch
  • Quick test: Add console.log in each transformer; you should see only one fire

Problem 4: “Transformer code has tons of if/else for field differences”

  • Why: You’re trying to do too much in one transformer class
  • Fix: Use composition: small, focused transformers that chain together
  • Quick test: If a transformer has >5 if statements, it’s doing too much

Project 5: The Legacy Adapter Layer (Backward Compatibility)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Go or Python
  • Alternative Programming Languages: Node.js, Ruby
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Architectural Patterns / Refactoring
  • Software or Tool: Docker (to simulate old/new services)
  • Main Book: “Monolith to Microservices” by Sam Newman

What you’ll build: You have a “Legacy” service and a “Modern” service. You will build an API Gateway/Adapter Layer that accepts requests for the old v1 API but translates and forwards them to the new Modern v2 service.

Why it teaches Versioning: This is the “Strangle Pattern” in action. You’ll learn how to migrate your backend entirely while keeping the public interface stable for old clients who refuse to upgrade.

Core challenges you’ll face:

  • Payload Translation → maps to mapping old JSON keys to new ones
  • Response Synthesis → maps to combining multiple new service calls to satisfy one old request
  • Error Mapping → maps to ensuring new error codes are translated back to v1 error codes

Real World Outcome

You can shut down your legacy database and code, yet old mobile apps still function perfectly by talking to your Adapter.

Example Output:

# Request to the ADAPTER (acting as the old API)
$ curl http://adapter.local/v1/legacy-endpoint
# The adapter calls Modern Service -> Transforms Result -> Returns to User
{ "status": "success", "old_field": "new_data" }

The Core Question You’re Answering

“How can I replace my backend entirely without breaking old clients?”

Before coding, understand this pattern: The Adapter is the “translator” between old contracts and new reality. It’s how companies migrate from monoliths to microservices without a “big bang” rewrite. This is the Strangler Fig pattern in action.


Concepts You Must Understand First

Stop and research these before coding:

  1. The Adapter Design Pattern
    • How does an adapter translate between incompatible interfaces?
    • Why is this better than modifying the new service to support old formats?
    • Book Reference: “Design Patterns” Ch. 4 - Gamma et al.
  2. The Strangler Fig Migration Pattern
    • How do you incrementally migrate without downtime?
    • What’s the difference between “strangler” and “big bang” migration?
    • Book Reference: “Monolith to Microservices” Ch. 3 - Sam Newman

Questions to Guide Your Design

Before implementing, think through these:

  1. Translation Logic
    • Does the adapter call one modern endpoint or multiple?
    • How do you handle fields that exist in v1 but not in the modern service?
  2. Error Handling
    • If the modern service returns 422 (Unprocessable Entity), should the adapter translate to 400 (Bad Request) for v1 clients?

Thinking Exercise

The “Multi-Call Synthesis”

Old API: GET /v1/user/123 returns { id, name, email, address, orderHistory }

New Architecture:

  • User Service: /users/123 → { id, name, email }
  • Address Service: /addresses?userId=123 → { street, city }
  • Order Service: /orders?userId=123 → [{ orderId, date }]

Questions while analyzing:

  • Does the adapter make 3 separate calls and combine the results?
  • What if one service is down? Return partial data or error?
  • How do you handle latency (now 3x slower than before)?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the Strangler Fig pattern and when you’d use it.”
  2. “How would you migrate from a monolith to microservices without API downtime?”
  3. “What are the performance implications of an adapter layer?”
  4. “How do you handle backwards-incompatible changes during migration?”
  5. “When would you retire the adapter layer entirely?”

Hints in Layers

Hint 1: Adapter as a proxy The adapter should act like a reverse proxy, listening on the old /v1/ paths but forwarding (transformed) to modern services.

Hint 2: Use HTTP client libraries In Go: net/http, in Python: requests or httpx, in Node: axios. Make the adapter a HTTP client to modern services.

Hint 3: Cache aggressively If the modern service is slower, add caching in the adapter to maintain old SLAs.

Hint 4: Versioned error codes Create an error mapping table: { 422: 400, 429: 503 } so modern errors become v1-compatible errors.


Books That Will Help

Topic Book Chapter
Adapter Pattern “Design Patterns” by Gamma et al. Ch. 4
Strangler Fig “Monolith to Microservices” by Sam Newman Ch. 3

Common Pitfalls & Debugging

Problem 1: “Adapter is too slow; timeout errors everywhere”

  • Why: You’re making synchronous calls to multiple services sequentially
  • Fix: Use async/parallel requests (Promise.all() in Node, asyncio.gather() in Python)
  • Quick test: Time a single adapter call vs direct modern service call

Problem 2: “One microservice is down and adapter crashes”

  • Why: No error handling; propagating exceptions
  • Fix: Implement circuit breaker pattern or return degraded responses
  • Quick test: Shut down one microservice; adapter should handle gracefully

Problem 3: “Old clients see new field names leaking through”

  • Why: Adapter is passing through modern responses without transformation
  • Fix: Explicit field mapping, never return modernResponse; directly
  • Quick test: Compare adapter response to old v1 spec character-by-character

Project 6: The Deprecation & Sunset Manager

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Any
  • Alternative Programming Languages: N/A (Standard based)
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: HTTP Standards / Communication
  • Software or Tool: Postman, Browser DevTools
  • Main Book: “Designing Web APIs” by Jin, Sahni, & Shevat

What you’ll build: A system that automatically injects Deprecation and Sunset headers into API responses for endpoints that are scheduled for retirement. You’ll also build a small dashboard that parses these headers to alert developers.

Why it teaches API Lifecycle: Designing an API is easy; retiring one is hard. This project teaches you how to communicate “The End” to your users programmatically, following IETF standards.

Core challenges you’ll face:

  • Implementing RFC 8594 → maps to understanding the Sunset header format
  • Middleware Integration → maps to injecting headers globally based on a deprecation config
  • Client-Side Awareness → maps to writing a fetch wrapper that logs warnings if it sees these headers

Key Concepts:

  • Sunset Header: RFC 8594
  • Deprecation Header: draft-ietf-httpapi-deprecation-header
  • Graceful Degradation: MDN Web Docs

Real World Outcome

Every response from a “dying” endpoint warns the developer in their console or logs.

Example Output:

$ curl -i http://api.local/v1/legacy-users

HTTP/1.1 200 OK
Deprecation: true
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Link: <https://docs.api.local/migration>; rel="deprecation"

[{"id": 1, "name": "Alice"}]

# Clients see headers warning them of upcoming retirement

The Core Question You’re Answering

“How do I communicate ‘The End’ to developers programmatically?”

Before coding, understand this: Developers don’t read your changelogs. They need machine-readable signals. RFC 8594 (Sunset header) and the Deprecation header spec provide standard ways to say “This endpoint will die on DATE. Migrate now.”


Concepts You Must Understand First

Stop and research these before coding:

  1. HTTP Sunset Header (RFC 8594)
    • What’s the exact date format for Sunset header? (HTTP-date)
    • Can you sunset individual fields, or only entire endpoints?
    • Book Reference: IETF RFC 8594 (available online)
  2. Graceful Degradation vs. Immediate Shutdown
    • Should deprecated endpoints still work fully, or return warnings?
    • At what point do you return 410 Gone instead of 200 OK?
    • Book Reference: “Designing Web APIs” Ch. 9 - Jin, Sahni, & Shevat

Questions to Guide Your Design

Before implementing, think through these:

  1. Deprecation Policy
    • How far in advance do you warn? (30 days? 6 months?)
    • Do different endpoint types get different deprecation windows?
  2. Client Detection
    • Should you log which clients are still hitting deprecated endpoints?
    • Do you need analytics to know when usage drops to zero?

Thinking Exercise

The “Timeline Paradox”

You announce deprecation on Jan 1, 2025. Sunset date is July 1, 2025.

On June 30, you still see 10,000 requests/day to the deprecated endpoint.

Questions while analyzing:

  • Do you extend the deadline (rewarding procrastinators)?
  • Do you shut down anyway (potentially breaking apps)?
  • How do you identify which clients are still using it?
  • Should you have different SLAs (slower responses, rate limits)?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the difference between Deprecation and Sunset headers.”
  2. “How long should a deprecation period be for a public API?”
  3. “What HTTP status code should you return after the sunset date?”
  4. “How would you handle clients that ignore deprecation warnings?”
  5. “Can you describe a real-world API deprecation that went badly?”

Hints in Layers

Hint 1: Middleware for headers Create middleware that checks if the requested path matches a deprecation config, then injects headers.

Hint 2: Config-driven Store deprecation metadata in a JSON/YAML file:

deprecated_endpoints:
  - path: /v1/users
    sunset: "2025-12-31T23:59:59Z"
    link: https://docs.api.local/v1-to-v2

Hint 3: Client dashboard Build a simple page that parses Sunset headers from responses and displays a migration checklist.

Hint 4: Gradual performance degradation After sunset date passes, add intentional delays (e.g., 5-second sleep) to deprecated endpoints as a “soft shutdown.”


Books That Will Help

Topic Book Chapter
API Evolution “Designing Web APIs” by Jin, Sahni, & Shevat Ch. 9
Deprecation Policy “Principles of Web API Design” by James Higginbotham Ch. 11

Common Pitfalls & Debugging

Problem 1: “Sunset header shows wrong date format”

  • Why: You’re using ISO 8601 instead of HTTP-date (RFC 7231)
  • Fix: Use format like Sat, 31 Dec 2025 23:59:59 GMT, not 2025-12-31T23:59:59Z
  • Quick test: Validate against HTTP-date regex or use a library

Problem 2: “Clients don’t see deprecation warnings in their logs”

  • Why: Most HTTP clients don’t log headers by default
  • Fix: Provide a SDK/wrapper that explicitly checks for and warns on these headers
  • Quick test: curl -i shows headers; does your client library expose them?

Problem 3: “After sunset date, endpoint still returns 200 OK”

  • Why: No logic to check if current date > sunset date
  • Fix: Middleware should check if (now > sunsetDate) return 410 Gone
  • Quick test: Manually set system clock forward and test

Project 7: The HATEOAS Navigator (Hypermedia Evolution)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Node.js or Java (Spring HATEOAS)
  • Alternative Programming Languages: Ruby (Grape), Python (Flask-RESTful)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: REST Architecture / Discovery
  • Software or Tool: HAL Browser, Postman
  • Main Book: “REST API Design Rulebook” by Mark Masse

What you’ll build: An API where the client doesn’t need to hardcode URLs. Each response contains links (Hypermedia) to available actions. You will evolve the API by changing the URLs while proving that a “smart client” doesn’t break.

Why it teaches Versioning: This is the “Holy Grail” of REST. You’ll understand how Hypermedia allows for out-of-band versioning, where the client follows links instead of constructing URLs. This is the ultimate defense against breaking changes.

Core challenges you’ll face:

  • Designing the Link Structure → maps to choosing between HAL, JSON-LD, or Siren formats
  • Dynamic Action Discovery → maps to showing/hiding links based on state (e.g., “cancel” link only if order is “pending”)
  • Client Implementation → maps to writing a generic navigator that “crawls” the API

Key Concepts:

  • HATEOAS: Wikipedia - HATEOAS
  • HAL (Hypertext Application Language): stateless.group - HAL
  • Richardson Maturity Model: Martin Fowler - Richardson Maturity Model

Real World Outcome

You can move your /users endpoint to /v2/accounts and your mobile app continues to work without an update because it just “followed the link.”

Example Output:

$ curl http://api.local/orders/123
{
  "id": 123,
  "status": "shipped",
  "_links": {
    "self": { "href": "/orders/123" },
    "track": { "href": "/shipping/v2/track/123" },
    "returns": { "href": "/returns/v1/new?orderId=123" }
  }
}

The Core Question You’re Answering

“What if the client doesn’t need to know the URL structure at all?”

Before coding, grasp this: HATEOAS (Hypermedia as the Engine of Application State) is the highest level of REST maturity. Instead of hardcoding /users/123/posts, the client follows _links.posts.href. This means you can change URLs without breaking clients.


Concepts You Must Understand First

Stop and research these before coding:

  1. Richardson Maturity Model
    • What are Levels 0-3 of REST maturity?
    • Why is HATEOAS considered “Level 3”?
    • Book Reference: Martin Fowler’s blog post on Richardson Maturity Model
  2. HAL (Hypertext Application Language)
    • How does HAL structure _links and _embedded?
    • What’s the difference between HAL and JSON-LD?
    • Book Reference: “REST API Design Rulebook” Ch. 6 - Mark Masse

Questions to Guide Your Design

Before implementing, think through these:

  1. Link Representation
    • Should links be absolute URLs or relative paths?
    • Do you include templated URLs (e.g., /orders/{id})?
  2. State-Dependent Links
    • Should a “shipped” order show a “cancel” link? (No!)
    • How do you conditionally include links based on resource state?

Thinking Exercise

The “Dumb Client, Smart Server”

Traditional client:

// Hardcoded knowledge
if (order.status === 'pending') {
  cancelUrl = `/orders/${order.id}/cancel`;
}

HATEOAS client:

// Follows what server provides
if (order._links.cancel) {
  cancelUrl = order._links.cancel.href;
}

Questions while analyzing:

  • Which approach lets you change the cancel URL path without updating clients?
  • Which approach lets you add new actions (like “refund”) without client changes?
  • What’s the tradeoff in response payload size?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain HATEOAS and why it’s rarely implemented in practice.”
  2. “What’s the difference between HAL, JSON-LD, and Siren formats?”
  3. “How does HATEOAS enable true API versioning without version numbers?”
  4. “What are the performance implications of including links in every response?”
  5. “Why might a mobile app avoid HATEOAS and hardcode URLs instead?”

Hints in Layers

Hint 1: Choose a hypermedia format HAL is simplest: { "data": {...}, "_links": { "self": {...}, "next": {...} } }

Hint 2: Link generation helpers Create a function addLinks(resource, baseUrl) that inspects resource state and builds appropriate links.

Hint 3: Conditional link inclusion

if (order.status === 'pending') {
  links.cancel = { href: `/orders/${order.id}/cancel`, method: 'DELETE' };
}

Hint 4: Client navigator Write a generic client that starts at a root URL and recursively follows links, never constructing URLs itself.


Books That Will Help

Topic Book Chapter
HATEOAS “REST API Design Rulebook” by Mark Masse Ch. 6
Hypermedia “RESTful Web APIs” by Leonard Richardson Ch. 6-7

Common Pitfalls & Debugging

Problem 1: “Clients still hardcode URLs despite providing HAL links”

  • Why: Habit; developers find it “easier” to construct URLs
  • Fix: Make URL structure intentionally unpredictable (random IDs, hashed paths) to force link-following
  • Quick test: Change a URL path; hardcoded clients break, HATEOAS clients don’t

Problem 2: “Response payload bloated with redundant links”

  • Why: Including every possible action even when not applicable
  • Fix: Conditional link generation based on resource state and user permissions
  • Quick test: A “shipped” order shouldn’t have 10 links; maybe only “track” and “return”

Problem 3: “Client gets stuck in infinite loop following links”

  • Why: Circular references (A links to B, B links back to A)
  • Fix: Track visited URLs in client, use rel attributes to identify link purpose
  • Quick test: Crawl your API from root; does it terminate or loop forever?

Project 8: The Schema Evolution Tester (DB vs API)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: SQL + Backend (any)
  • Alternative Programming Languages: Python (Alembic), Node (Knex)
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 4: Expert
  • Knowledge Area: Database Design / Data Integrity
  • Software or Tool: PostgreSQL, Liquibase
  • Main Book: “Monolith to Microservices” by Sam Newman

What you’ll build: A system where you refactor a database column (e.g., splitting name into first_name and last_name) while keeping an API v1 alive that still expects a single name field. You will implement the migration using the Expand/Contract Pattern.

Why it teaches Versioning: Most API breaks happen because of database changes. This project teaches you how to decouple your database schema from your API schema. You’ll learn to use database views or application-level mapping to maintain backward compatibility.

Core challenges you’ll face:

  • Dual-Writing → maps to writing to both old and new columns during migration
  • Backward Compatible Views → maps to using SQL views to fake the old table structure
  • Performance Impact → maps to measuring the overhead of the mapping layer

Difficulty: Expert Time estimate: 2 weeks Prerequisites: Understanding of Database Normalization and SQL.


The Core Question You’re Answering

“How do I change my database schema without breaking the API contract?”

Before coding, understand this truth: Your database is an implementation detail. Your API is a public contract. They must evolve independently. The Expand/Contract pattern (also called “Parallel Change”) lets you make breaking database changes without API downtime.


Concepts You Must Understand First

Stop and research these before coding:

  1. The Expand/Contract Pattern
    • What’s the three-phase migration? (Expand → Migrate → Contract)
    • How long do you maintain dual-write state?
    • Book Reference: “Refactoring Databases” by Ambler & Sadalage
  2. Database Views for Compatibility
    • How can a SQL view fake the old schema?
    • What’s the performance impact of view-based abstractions?
    • Book Reference: “Monolith to Microservices” Ch. 4 - Sam Newman

Questions to Guide Your Design

Before implementing, think through these:

  1. Migration Strategy
    • Do you dual-write (to both old and new columns) during transition?
    • How do you backfill old data into the new schema?
  2. Rollback Plan
    • If something breaks, can you roll back the migration without data loss?
    • Do you keep old columns until 100% of traffic migrates?

Thinking Exercise

The “Name Split Migration”

Current DB: users table with name VARCHAR(100)

Target: Split into first_name and last_name

Phase 1 (Expand): Add first_name, last_name columns Phase 2 (Migrate): Application writes to both name and first_name/last_name Phase 3 (Contract): Drop name column

Questions while analyzing:

  • What if a user has only one name (like “Madonna”)?
  • During Phase 2, do you trust name or first_name + last_name as source of truth?
  • How do you test that v1 API still works during all 3 phases?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain the Expand/Contract pattern for database migrations.”
  2. “How would you split a column without API downtime?”
  3. “What’s the difference between a migration and a refactoring?”
  4. “How do you handle data inconsistencies during dual-write phases?”
  5. “Can you describe a database migration that went wrong and how you’d fix it?”

Hints in Layers

Hint 1: Add new columns first (Expand) Use an ALTER TABLE to add first_name and last_name as nullable columns initially.

Hint 2: Dual-write application logic (Migrate) Modify your API to write to both schemas: name for v1 clients, first_name/last_name for new logic.

Hint 3: Use database triggers for backfilling Create a trigger that auto-populates first_name/last_name when name is updated (or vice versa).

Hint 4: Views for backward compatibility (Contract) Create a view that synthesizes the old name column: CREATE VIEW users_v1 AS SELECT id, CONCAT(first_name, ' ', last_name) AS name FROM users.


Books That Will Help

Topic Book Chapter
Database Refactoring “Refactoring Databases” by Ambler & Sadalage Ch. 3
Schema Evolution “Monolith to Microservices” by Sam Newman Ch. 4

Common Pitfalls & Debugging

Problem 1: “Data in old column doesn’t match new columns”

  • Why: Dual-write logic isn’t symmetric; one path writes correctly, other doesn’t
  • Fix: Write comprehensive tests that verify data consistency across both schemas
  • Quick test: Insert via v1 API, read via direct DB query of new columns; should match

Problem 2: “Performance degraded after adding view”

  • Why: Views add query overhead, especially with complex CONCAT or JOIN operations
  • Fix: Add indexes on new columns, or consider materialized views
  • Quick test: EXPLAIN your view query and check for full table scans

Problem 3: “Migration script fails halfway; database in inconsistent state”

  • Why: Migration wasn’t wrapped in a transaction or migration tool
  • Fix: Use migration frameworks (Alembic, Flyway, Liquibase) with automatic rollback
  • Quick test: Intentionally break migration mid-way; can you recover?

Project 9: The API Diffing Engine (CI/CD Safety)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Python or Go
  • Alternative Programming Languages: Rust, Bash
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: DevOps / Automation
  • Software or Tool: openapi-diff, GitHub Actions
  • Main Book: “Designing Web APIs” by Jin, Sahni, & Shevat

What you’ll build: A tool that compares two OpenAPI specifications and identifies Breaking Changes (e.g., removed field, changed type, new required parameter). You will integrate this into a Git workflow that blocks Pull Requests if they break the contract without a version bump.

Why it teaches API Design: You will learn exactly what constitutes a breaking change by building the logic that detects them. You’ll categorize changes into “Safe” (Additions) and “Breaking” (Removals/Changes).

Core challenges you’ll face:

  • Semantic Diffing → maps to ignoring formatting changes and focusing on structural changes
  • Detecting Optional vs Required → maps to understanding that making an optional field required is a break
  • CLI Design → maps to providing clear output for developers to fix their specs

Real World Outcome

A GitHub Action that comments on PRs: “🚨 WARNING: This PR introduces a breaking change to the /v1/users endpoint. Please bump the version or fix the schema.”

Example Output:

$ git diff main feature/api-v2 -- openapi.yaml | ./api-diff

🚨 BREAKING CHANGES DETECTED:

1. /users endpoint
   - REMOVED field: "username" (was required)
   - CHANGED type: "age" from integer to string

2. /orders endpoint
   - ADDED required field: "shipping_address"

✅ SAFE CHANGES:

1. /products endpoint
   - ADDED optional field: "tags"

RECOMMENDATION: Bump major version (v2 → v3)

The Core Question You’re Answering

“How can I prevent accidental breaking changes from reaching production?”

Before coding, understand this: Humans make mistakes. Code reviews miss subtle breaks. Automated diffing catches what humans miss. This project builds the “safety net” for your API evolution.


Concepts You Must Understand First

Stop and research these before coding:

  1. Semantic Versioning for APIs
    • What constitutes a MAJOR vs MINOR vs PATCH change?
    • Is adding a required field breaking? (Yes!)
    • Book Reference: “API Design Patterns” Ch. 22 - JJ Geewax
  2. AST (Abstract Syntax Tree) Diffing
    • How do you compare two JSON/YAML structures semantically, not textually?
    • What’s the difference between git diff and semantic diffing?
    • Book Reference: “Designing Web APIs” Ch. 9 - Jin, Sahni, & Shevat

Questions to Guide Your Design

Before implementing, think through these:

  1. Breaking Change Detection
    • Is renaming a field breaking? (Yes, even if the data type is the same)
    • Is changing from type: string to type: [string, null] breaking? (No, it’s more permissive)
  2. CI Integration
    • Should the diff tool block the PR automatically, or just warn?
    • How do you allow intentional breaking changes (with manual override)?

Thinking Exercise

The “Subtle Break”

v1 spec:

email:
  type: string
  pattern: "^[a-z@.]+$"

v2 spec:

email:
  type: string
  pattern: "^[A-Za-z0-9@.]+$"

Questions while analyzing:

  • Is this breaking? (It’s more permissive, so… no!)
  • But wait—old clients might not handle uppercase. Is it still safe?
  • How do you detect pattern/regex changes in your diff tool?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you automatically detect breaking changes in an OpenAPI spec?”
  2. “What’s the difference between backward-compatible and forward-compatible changes?”
  3. “Explain how semantic versioning applies to APIs.”
  4. “How would you handle a team that wants to make a breaking change without bumping the major version?”
  5. “Can you describe a change that seems safe but is actually breaking?”

Hints in Layers

Hint 1: Use existing diff libraries Don’t parse YAML manually. Use libraries like openapi-diff, oasdiff, or swagger-diff.

Hint 2: Categorize changes Create an enum: BREAKING | POTENTIALLY_BREAKING | SAFE. Map each detected change to a category.

Hint 3: Exit codes for CI Return exit code 1 if breaking changes detected, 0 otherwise. CI can fail the build on non-zero exit.

Hint 4: Allow manual override Support a flag like --allow-breaking or a commit message footer [breaking-change-approved] to bypass the check.


Books That Will Help

Topic Book Chapter
API Evolution “Designing Web APIs” by Jin, Sahni, & Shevat Ch. 9
Semantic Versioning “API Design Patterns” by JJ Geewax Ch. 22

Common Pitfalls & Debugging

Problem 1: “Diff tool says everything is breaking, even spacing changes”

  • Why: You’re doing textual diff, not semantic diff
  • Fix: Parse both specs into objects, compare the AST, ignore formatting
  • Quick test: Reformat a spec with different indentation; tool should report no changes

Problem 2: “Tool misses breaking changes like removing an endpoint”

  • Why: You’re only checking schema changes, not path changes
  • Fix: Also diff the paths section, not just components.schemas
  • Quick test: Delete an endpoint; tool should scream “BREAKING”

Problem 3: “CI always fails even on safe PRs”

  • Why: Tool is too strict, flagging optional field additions as breaking
  • Fix: Implement proper breaking change logic: removing required field = breaking, adding optional = safe
  • Quick test: Add optional field; should pass. Remove required field; should fail.

Project 10: The Idempotency Key Manager (Safety First)

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Node.js or Python
  • Alternative Programming Languages: Go, Java
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Distributed Systems / Transactional Integrity
  • Software or Tool: Redis, PostgreSQL
  • Main Book: “API Design Patterns” by JJ Geewax

What you’ll build: A middleware that handles Idempotency-Key headers. If a client retries a POST request with the same key, your API must return the cached response of the first successful request instead of performing the action again.

Why it teaches API Design: One of the biggest challenges in API design is “at-least-once” delivery. You’ll learn how to make your API “safe” for unreliable networks. This is a core requirement for payments and financial APIs.

Core challenges you’ll face:

  • Atomic Storage → maps to ensuring you store the key and the result atomically
  • Handling Race Conditions → maps to what happens if two identical requests arrive at the exact same millisecond?
  • Response Caching → maps to storing headers and status codes, not just the body

Key Concepts:

  • Idempotency: Stripe Engineering Blog - Idempotency
  • Distributed Locking: Redis Redlock
  • Transactional Outbox: Microservices.io - Transactional Outbox

Real World Outcome

You can click “Pay” 10 times on a slow connection, but the user is only charged once.

Example Output:

# First request
$ curl -X POST -H "Idempotency-Key: 123" http://api.local/charge -d '{"amount": 10}'
HTTP/1.1 201 Created
{ "transaction_id": "abc", "status": "success" }

# Immediate retry
$ curl -X POST -H "Idempotency-Key: 123" http://api.local/charge -d '{"amount": 10}'
HTTP/1.1 200 OK (From Cache)
{ "transaction_id": "abc", "status": "success" }

The Core Question You’re Answering

“How do I make my API safe for unreliable networks and impatient users?”

Before coding, understand this: The internet is unreliable. Clients retry. Users double-click. Without idempotency, you charge their credit card twice. This project teaches you to make write operations safe to retry.


Concepts You Must Understand First

Stop and research these before coding:

  1. Idempotency in Distributed Systems
    • What’s the difference between “at-most-once” and “exactly-once” delivery?
    • Why are GET/DELETE naturally idempotent but POST is not?
    • Book Reference: “Designing Data-Intensive Applications” Ch. 11 - Martin Kleppmann
  2. Distributed Locking
    • How do you prevent two identical requests from executing simultaneously?
    • What’s the Redlock algorithm?
    • Book Reference: “API Design Patterns” Ch. 13 - JJ Geewax

Questions to Guide Your Design

Before implementing, think through these:

  1. Key Storage
    • How long do you cache idempotency keys? (24 hours? Forever?)
    • Do you store just the response body or also headers and status code?
  2. Race Conditions
    • What happens if two requests with the same key arrive in the same millisecond?
    • Do you use pessimistic locking (Redis lock) or optimistic (unique constraint)?

Thinking Exercise

The “Double-Submit Problem”

User clicks “Pay $100” button twice within 10ms.

Without idempotency:

  • Request 1: Charge $100 → Success
  • Request 2: Charge $100 → Success (OOPS! $200 charged)

With idempotency keys:

  • Request 1: Store key “abc123” → Charge $100 → Cache response
  • Request 2: Key “abc123” exists → Return cached response (no second charge!)

Questions while analyzing:

  • What if Request 1 is still processing when Request 2 arrives?
  • Should Request 2 wait (blocking) or return 409 Conflict?
  • How do you handle the case where Request 1 fails but Request 2 succeeds?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain idempotency and why it’s critical for payment APIs.”
  2. “How would you implement idempotency keys in a distributed system?”
  3. “What’s the difference between idempotency and deduplication?”
  4. “How long should you cache idempotency keys?”
  5. “What happens if a client retries with the same key but different payload?”

Hints in Layers

Hint 1: Use Redis for key storage Store idempotency_key -> { status_code, headers, body, timestamp } with a TTL (e.g., 24 hours).

Hint 2: Atomic check-and-set Use Redis SETNX (set if not exists) to atomically claim the key before processing.

Hint 3: Handle in-flight requests If the key exists but the response isn’t cached yet, return 409 Conflict or wait with a timeout.

Hint 4: Validate payload consistency If the same key is used with a different request body, return 422 Unprocessable Entity.


Books That Will Help

Topic Book Chapter
Idempotency “API Design Patterns” by JJ Geewax Ch. 13
Distributed Systems “Designing Data-Intensive Applications” by Martin Kleppmann Ch. 11

Common Pitfalls & Debugging

Problem 1: “Same request processed twice despite idempotency key”

  • Why: Race condition; both requests see key doesn’t exist simultaneously
  • Fix: Use atomic operations like Redis SETNX or database unique constraints
  • Quick test: Send 100 concurrent requests with same key; should process exactly once

Problem 2: “Cached response has stale data”

  • Why: TTL is too long; underlying data changed after caching
  • Fix: Shorter TTL (24 hours max) or invalidate cache when resource changes
  • Quick test: Make request, modify resource directly, retry with same key; should see old data (this is correct!)

Problem 3: “Client sends same key but different payload; should that be allowed?”

  • Why: Design decision—Stripe treats this as an error
  • Fix: Hash the request payload and store it with the key; if payload doesn’t match, return 422
  • Quick test: Send key “abc” with {"amount": 100}, retry with {"amount": 200}; should reject second

Project 11: The Stripe-Style Date Versioner

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: TypeScript
  • Alternative Programming Languages: Ruby, Python
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 5: Master
  • Knowledge Area: Software Architecture / Metaprogramming
  • Software or Tool: Any highly dynamic language
  • Main Book: “Stripe’s API Versioning” (Blog/Articles)

What you’ll build: A versioning system based on dates (e.g., 2024-12-28). Instead of v1/v2, the backend always runs the “Current” code, and a series of “Version Transformers” (Middleware) intercept the request/response to transform data back to the format requested by the client’s version date.

Why it teaches Versioning: This is the most sophisticated versioning model in existence. You’ll learn how to avoid “branching” entirely by using a transformation pipeline. This project represents the pinnacle of backward compatibility.

Core challenges you’ll face:

  • The Transformation Pipeline → maps to chaining multiple “undo” operations to reach an old version
  • Schema Snapshots → maps to knowing what the API looked like on a specific date
  • Maintainability → maps to how to add a transformation without breaking the pipeline

Real World Outcome

You can release daily updates to your API, and a client using a 2-year-old version date still sees exactly what they expect.

Example Output:

# Client pinned to an old version
$ curl -H "API-Version: 2022-01-01" http://api.local/users/1
{ "name": "Alice" }

# Current version
$ curl -H "API-Version: 2024-12-28" http://api.local/users/1
{ "firstName": "Alice", "lastName": "Smith" }

The Core Question You’re Answering

“Can I evolve my API daily without ever breaking old clients?”

Before coding, understand this is the pinnacle: Stripe’s date-based versioning. Instead of v1/v2/v3, you have 2024-01-15, 2024-03-20, etc. The backend always runs the latest code, and transformers “undo” changes to make responses match old dates. This eliminates branching entirely.


Concepts You Must Understand First

Stop and research these before coding:

  1. The Transformation Pipeline Pattern
    • How do you chain reversible transformations?
    • What’s the difference between “undo” and “redo” transformations?
    • Book Reference: Stripe Engineering Blog - API Versioning
  2. Migration Scripts as Code
    • How do you represent schema changes as executable functions?
    • Can you “reverse” a transformation mathematically?
    • Book Reference: “Refactoring Databases” - Ambler & Sadalage

Questions to Guide Your Design

Before implementing, think through these:

  1. Transformation Order
    • If you have 100 versions, do transformations run sequentially (slow) or can they be optimized?
    • How do you handle non-reversible changes (e.g., data deleted)?
  2. Version Metadata
    • How do you know what your API looked like on 2022-05-15?
    • Do you snapshot the spec on each version date?

Thinking Exercise

The “Time Travel Transformation”

Current version (2024-12-28): { "user": { "firstName": "Alice", "lastName": "Smith" } }

Client requests API-Version: 2022-01-01.

Transformations to apply (in reverse chronological order):

  1. 2024-01-15: Split name into firstName/lastName → REVERSE: Combine into name
  2. 2023-06-20: Nested user data under user key → REVERSE: Flatten to root
  3. 2022-06-01: Renamed username to name → REVERSE: Rename back to username

Result: { "username": "Alice Smith" }

Questions while analyzing:

  • How do you store these transformations (as code? as config?)?
  • What if a transformation can’t be reversed (data deleted)?
  • How do you test that 100 transformations don’t break?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “Explain how Stripe’s date-based API versioning works.”
  2. “What are the advantages and disadvantages compared to URI versioning?”
  3. “How would you implement a reversible transformation pipeline?”
  4. “What happens if a client requests a version from before your API existed?”
  5. “How do you handle non-reversible changes in this model?”

Hints in Layers

Hint 1: Store transformations as functions Create a registry: { "2024-01-15": { forward: splitName, backward: combineName } }

Hint 2: Apply transformations conditionally If client version < transformation date, apply the backward transformation.

Hint 3: Use middleware for version resolution Extract the API-Version header, determine which transformations to apply, attach to request context.

Hint 4: Cache transformed responses If many clients use the same old version, cache the transformed response to avoid recomputing.


Books That Will Help

Topic Book Chapter
Stripe’s Approach Stripe Engineering Blog API Versioning article
Reversible Migrations “Refactoring Databases” by Ambler & Sadalage Ch. 5

Common Pitfalls & Debugging

Problem 1: “Transformation pipeline is too slow for production”

  • Why: Running 50+ transformations sequentially on every request
  • Fix: Combine/optimize transformations, or pre-generate responses for popular old versions
  • Quick test: Benchmark request with version=2020-01-01 vs current; should be <10ms difference

Problem 2: “A transformation broke; can’t serve old versions”

  • Why: One backward transformation has a bug
  • Fix: Comprehensive tests for each transformation with real data
  • Quick test: Request every historical version; all should return 200

Problem 3: “Client requests version from before API existed”

  • Why: No validation on version header
  • Fix: Return 400 Bad Request with Earliest-Supported-Version: 2022-01-01 header
  • Quick test: Request version=1999-01-01; should get clear error

Project 12: The Multi-Tenant API Gateway Router

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Go or Node.js
  • Alternative Programming Languages: Rust (Axum/Tower), Nginx (Lua)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Proxy Servers / Infrastructure
  • Software or Tool: Envoy, Nginx, or a custom Proxy
  • Main Book: “API Gateways” by Madhusudhan Konda

What you’ll build: A gateway that sits in front of your microservices. It looks up a client’s API Key in a database, finds their “Pinned Version,” and automatically routes their request to the correct backend service version.

Why it teaches Infrastructure: This project shows how versioning is managed at the network layer. You’ll learn how to separate the “Who” (the client) from the “What” (the version) and “Where” (the service).

Core challenges you’ll face:

  • High-Performance Lookups → maps to using a cache (Redis) for API keys so you don’t hit the DB for every request
  • Dynamic Routing → maps to changing the target URL on the fly based on client metadata
  • Observability → maps to logging which versions are being used by which clients

Real World Outcome

You can “force upgrade” a specific client to v2 by simply changing a value in your admin database, without them changing a single line of code.

Example Output:

# Client A (Pinned to v1)
$ curl -H "Authorization: Bearer KEY_A" http://gateway.local/data
-> Proxying to http://internal-service:8001/v1/data

# Client B (Pinned to v2)
$ curl -H "Authorization: Bearer KEY_B" http://gateway.local/data
-> Proxying to http://internal-service:8002/v2/data

# You control versions centrally, without client changes!

The Core Question You’re Answering

“How can I manage versions centrally without forcing every client to change headers?”

Before coding, understand this pattern: Instead of clients specifying versions, the gateway knows which version each client should use (based on API key, tenant ID, or account metadata). This gives you centralized control over migrations.


Concepts You Must Understand First

Stop and research these before coding:

  1. API Gateway Patterns
    • What’s the difference between a reverse proxy and an API gateway?
    • How do you implement dynamic routing based on metadata?
    • Book Reference: “Building Microservices” Ch. 11 - Sam Newman
  2. Multi-Tenancy
    • What’s tenant isolation and why does it matter?
    • How do you store per-tenant configuration (versions, rate limits)?
    • Book Reference: “API Gateways” - Madhusudhan Konda

Questions to Guide Your Design

Before implementing, think through these:

  1. Client Identification
    • Do you use API keys, JWTs, or OAuth tokens?
    • Where do you store the version mapping (database, Redis, config file)?
  2. Performance
    • How do you avoid a database lookup on every request?
    • Should you cache client-to-version mappings in memory?

Thinking Exercise

The “Version Override Matrix”

You have:

  • 1000 clients
  • 3 API versions (v1, v2, v3)
  • 95% should use v2 (stable)
  • 5 beta clients on v3 (testing)
  • 1 legacy client stuck on v1

Questions while analyzing:

  • Is it better to store “default: v2, exceptions: […]” or individual mappings per client?
  • How do you migrate a client from v1 → v2 without them changing code?
  • What if you want to A/B test v3 with 10% of traffic?

The Interview Questions They’ll Ask

Prepare to answer these:

  1. “What’s the difference between an API gateway and a load balancer?”
  2. “How would you implement per-client versioning in a gateway?”
  3. “What are the performance implications of looking up client metadata on every request?”
  4. “How would you handle a gradual rollout (e.g., 10% on v2, 90% on v1)?”
  5. “Can you describe the tradeoffs between client-driven and server-driven versioning?”

Hints in Layers

Hint 1: Extract client identifier Parse the Authorization: Bearer <token> header and extract client ID (from JWT or API key lookup).

Hint 2: Version lookup with caching Query a database/Redis for client_id -> version, but cache aggressively (in-memory LRU cache).

Hint 3: Dynamic proxy target Based on version, modify the upstream URL: http://service-v1:8001 vs http://service-v2:8002.

Hint 4: Observability Log every request with { client_id, version, upstream, latency } for analytics on version usage.


Books That Will Help

Topic Book Chapter
API Gateways “Building Microservices” by Sam Newman Ch. 11
Gateway Patterns “API Gateways” by Madhusudhan Konda Ch. 3-4

Common Pitfalls & Debugging

Problem 1: “Gateway is the bottleneck; everything is slow”

  • Why: Doing expensive lookups (database query) on every request
  • Fix: Aggressive caching (in-memory with TTL) of client-to-version mappings
  • Quick test: Benchmark gateway latency; should add <5ms overhead

Problem 2: “Changed client version in database but still routing to old version”

  • Why: Cached mapping is stale
  • Fix: Implement cache invalidation (pub/sub on version changes) or shorter TTL
  • Quick test: Update version, wait for TTL expiry, check routing

Problem 3: “Some clients authenticated but version routing fails”

  • Why: New clients don’t have version mapping yet
  • Fix: Default fallback version (e.g., always use latest if no mapping exists)
  • Quick test: Create new API key, make request without version mapping; should work

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
1. Spec Architect Level 1 Weekend High (Design) ⭐⭐⭐
4. Strategy Patterns Level 3 1-2 Weeks High (Code structure) ⭐⭐⭐⭐
8. Schema Evolution Level 4 2 Weeks Very High (Systems) ⭐⭐⭐
9. CI/CD Diffing Level 3 1 Week High (DevOps) ⭐⭐⭐⭐
11. Stripe-Style Level 5 1 Month Master (Evolution) ⭐⭐⭐⭐⭐

Recommendation

For beginners: Start with Project 1 and Project 2. You must learn to “speak” API via OpenAPI before you can build them. Understanding the contract is 50% of the battle.

For senior engineers: Jump straight to Project 4 and Project 11. These projects address the “spaghetti code” that inevitably happens when an API lives for more than a year.


Final Overall Project: The Immortal API

  • File: API_DESIGN_VERSIONING_MASTERY.md
  • Main Programming Language: Node.js/TypeScript or Go
  • Business Potential: 5. The “Industry Disruptor”
  • Difficulty: Level 5: Master
  • Knowledge Area: Full Lifecycle API Management

What you’ll build: A complete, multi-versioned “Banking as a Service” API that implements every concept learned:

  1. Contract-First: A 1000+ line OpenAPI spec.
  2. Versioned Gateway: Routing by API-Key/Pinned Version.
  3. Strategy Pattern: Clean, transformer-based versioning logic.
  4. Idempotency: Safe payment processing.
  5. Sunset Logic: Automated deprecation headers.
  6. CI/CD: Automatic breaking-change detection.

Why it teaches API Design: This project forces you to integrate all the isolated skills into a single, production-ready system. You’ll see how design decisions in the spec (Project 1) impact your implementation (Project 4) and your deployment (Project 9).


Real World Outcome

A system that you can evolve for years without ever sending a “Sorry, we broke your app” email to your users.

Example Output:

$ git push origin main
[CI/CD] Comparing specs... No breaking changes detected for v2.
[CI/CD] Deploying v2.3...
[Gateway] v1.0 clients still routing to Transformer V1.
[Gateway] v2.0 clients now seeing new 'rewards' field.

The Core Question You’re Answering

“Can I build a system that never has to stop, never has to break, and can grow forever?”


Thinking Exercise

The “Universal Transformation”

Imagine a request comes in for a version from 3 years ago (2021-01-01). Your database now has 5 fields that didn’t exist then, and 2 fields that were renamed twice.

Questions while analyzing:

  • Does your Strategy pattern handle the request (incoming data) or the response (outgoing data)? Or both?
  • If you have 50 versions, do you write 50 separate transformers, or a pipeline of 50 small changes?
  • How do you handle a change where the data type changed from a String to an Object?

Hints in Layers

Hint 1: The Layered Architecture Build the gateway first. It should identify the user and their version. Then build the core service which only speaks the “Current” version.

Hint 2: The Middleware Pipeline For versioning, use a pipeline. If the client is on v1 and the current is v3, the request goes through v1_to_v2 then v2_to_v3. The response goes back through v3_to_v2 then v2_to_v1.

Hint 3: Use a Document Store or Flexible Schemas For the backend, using a flexible schema (like JSONB in Postgres) can make it easier to store new metadata without breaking old transformers that don’t know it exists.

Hint 4: Automated Testing is Mandatory You cannot build this without a test suite that runs the SAME tests against every supported version. Use a tool like Dredd or custom Jest tests to ensure v1, v2, and v3 all pass their respective contracts.


The Interview Questions They’ll Ask

  1. “Walk me through your process for implementing a breaking change in a high-traffic API.”
  2. “How do you ensure data consistency between different API versions sharing one database?”
  3. “What are the trade-offs between ‘Contract-First’ and ‘Code-First’ development?”
  4. “How do you detect and prevent accidental breaking changes in a large team?”
  5. “If a client is stuck on v1 but you need to delete that database table, what do you do?”

Summary

This learning path covers API Design & Versioning through 12 hands-on projects. Here’s the complete list:

# Project Name Main Language Difficulty Time Estimate
1 The API Spec Architect YAML (OAS) Beginner Weekend
2 The Contract Validator Node.js Intermediate 1 week
3 Multi-Version Router (URI) TypeScript Intermediate 1-2 weeks
4 Strategy Pattern Implementer TypeScript Advanced 1-2 weeks
5 Legacy Adapter Layer Go Advanced 2 weeks
6 Deprecation Manager Any Intermediate Weekend
7 HATEOAS Navigator Node.js Advanced 2 weeks
8 Schema Evolution Tester SQL/Any Expert 2 weeks
9 API Diffing Engine Python/Go Advanced 1 week
10 Idempotency Key Manager Node.js Advanced 1-2 weeks
11 Stripe-Style Date Versioner TypeScript Master 1 month
12 Multi-Tenant Gateway Go Expert 2 weeks

Expected Outcomes

After completing these projects, you will:

  • Write industrial-grade OpenAPI 3.1 specifications.
  • Understand the technical and social cost of breaking changes.
  • Implement advanced architectural patterns (Strategy, Adapter) for versioning.
  • Manage the entire API lifecycle from design to deprecation.
  • Build CI/CD pipelines that protect your API contract.

You’ll have built 12 working projects that demonstrate deep understanding of API Design and Versioning from first principles.

Core Concept Analysis

The API Lifecycle: From Spec to Sunset

   [ Design ] --> [ Specification ] --> [ Implementation ] --> [ Evolution ]
       ^              (OpenAPI)               |
       |
   [ Feedback ] <----------------------- [ Monitoring ] <--------+

1. Resource-Oriented Design

Instead of thinking in “functions” (getUser()), think in “resources” (GET /users/{id}).

  • Nouns, not Verbs: /orders, not /getOrders.
  • Hierarchies: /users/123/posts/456.
  • Idempotency: Ensuring PUT and DELETE can be called multiple times safely.

2. The OpenAPI Specification (OAS)

The source of truth. It defines:

  • Paths: The endpoints.
  • Components: Reusable schemas (models).
  • Security: How clients authenticate.
  • Examples: What the data actually looks like.

3. Versioning Strategies (The “Why”)

URI Versioning        Header Versioning      Media Type Versioning
(Standard)            (Clean URLs)           (Granular)
/v1/users             Accept-Version: 1      Accept: application/vnd.app.v1+json

4. The Strategy Pattern for Versioning

When a request comes in, a “Version Selector” (The Strategy) identifies the version and routes it to the correct “Handler” or “Transformer”. This keeps your controller logic clean.

       [ Request ]
            |
    [ Version Resolver ]
     /      |       \
[v1 Logic] [v2 Logic] [v3 Logic]
     \      |       /
       [ Database ]

5. Backward Compatibility & “Tombstoning”

How to remove fields without breaking old clients.

  • Expansion: Adding fields (Safe).
  • Contraction: Removing/Renaming fields (Breaking).
  • Transformation: Using Adapters to map new DB fields back to old API fields.