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:
- HTTP Fundamentals
- Understanding of GET, POST, PUT, PATCH, DELETE methods
- Knowledge of status codes (200, 201, 400, 404, 500)
- Headers and request/response structure
- JSON Data Structures
- Object and array syntax
- Nested structures and references
- Backend Programming
- Proficiency in at least one language (JavaScript, Python, Go, Java)
- Understanding of web frameworks (Express, FastAPI, Flask, Spring)
- 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
- Foundation (Week 1):
- Principles of Web API Design Ch. 1-3 (Aligning with goals)
- The Design of Web APIs Ch. 3-4 (Resource design)
- The Contract (Week 2):
- OpenAPI 3.1 Specification (Official Docs - read the structure)
- The Design of Web APIs Ch. 12
- 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)
- Read Arnaud Lauret’s “The Design of Web APIs” Ch. 3-4 (Resource design)
- Explore 3 real-world API docs: Stripe, GitHub, Twilio
- Compare their versioning approaches
Day 2: Your First Spec (4 hours)
- Start Project 1 (API Spec Architect)
- Model a simple “Blog API” with posts and comments
- Validate it in Swagger Editor
- 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.
Recommended Learning Paths
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:
- 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
- OpenAPI 3.x Structure
- What is the purpose of the
componentssection? - How does
$refwork for reusability? - Book Reference: “The Design of Web APIs” Ch. 12
- What is the purpose of the
Questions to Guide Your Design
Before implementing, think through these:
- Entity Relationships
- How should a “Transaction” link to a “User”? By ID or by full object?
- Should
GET /accountsreturn the full transaction history or just a link?
- 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
middleNamea 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:
- “Explain the difference between PUT and PATCH and when to use each.”
- “What makes a change ‘breaking’ in an API?”
- “How do you handle pagination in a RESTful way?”
- “Why is the OpenAPI spec valuable for frontend teams?”
- “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
$refpath or the referenced component doesn’t exist - Fix: Check that
$ref: '#/components/schemas/User'matches exactly the name in yourcomponents.schemassection (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
$reffor reusability - Fix: Define common responses in
components.responsesand reference them:$ref: '#/components/responses/NotFound' - Quick test: If you’re copy-pasting YAML, you need more
$refusage
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: trueand 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-matchesrule 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=123to 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:
- 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
- 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:
- Linting Rules
- Should you enforce kebab-case or snake_case in paths?
- Do all POST endpoints need to return 201? What about idempotent creates?
- 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:
- “What are the benefits of contract-first API development?”
- “How would you enforce API design standards across a team of 20 developers?”
- “Explain how a mock server can speed up frontend development.”
- “What’s the difference between validation at the gateway vs. validation in the service?”
- “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.,
/userProfilevs/user-profile) - Fix: Choose a convention (kebab-case recommended) and apply it everywhere
- Quick test: Run
spectral lintwith thepath-caserule
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-matchesrule - Quick test: Compare your
exampleagainst yourtypedefinition
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:
- URI Versioning vs. Other Strategies
- Why is
/v1/usersbetter than/users?version=1? - How does this affect HTTP caching (CDN, browser cache)?
- Book Reference: “API Design Patterns” Ch. 22 - JJ Geewax
- Why is
- 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:
- Routing Strategy
- Do you create separate Express routers for
/v1/and/v2/? - Should version detection happen in middleware or per-route?
- Do you create separate Express routers for
- Data Transformation
- Do you transform at the database layer or presentation layer?
- If
user_namein DB becomesfullNamein 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:
- “What are the pros and cons of URI versioning compared to header-based versioning?”
- “How would you handle a breaking database schema change in a versioned API?”
- “Explain how you would deprecate v1 while keeping it functional for legacy clients.”
- “How does URI versioning affect HTTP caching?”
- “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), notapp.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:
- 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)
- HTTP Content Negotiation
- What are the
AcceptandContent-Typeheaders? - How does a server decide which version to return when multiple are supported?
- Book Reference: “API Design Patterns” Ch. 22
- What are the
Questions to Guide Your Design
Before implementing, think through these:
- 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?)
- 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:
- “What are the pros and cons of URI versioning vs Header versioning?”
- “How would you use the Strategy Pattern to manage API versioning?”
- “What HTTP status code should you return for an unsupported version?”
- “How do you handle default versions for existing clients?”
- “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-Versionvsaccept-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/elseorswitch - 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:
- 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.
- 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:
- 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?
- 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:
- “Explain the Strangler Fig pattern and when you’d use it.”
- “How would you migrate from a monolith to microservices without API downtime?”
- “What are the performance implications of an adapter layer?”
- “How do you handle backwards-incompatible changes during migration?”
- “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:
- 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)
- 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:
- Deprecation Policy
- How far in advance do you warn? (30 days? 6 months?)
- Do different endpoint types get different deprecation windows?
- 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:
- “Explain the difference between Deprecation and Sunset headers.”
- “How long should a deprecation period be for a public API?”
- “What HTTP status code should you return after the sunset date?”
- “How would you handle clients that ignore deprecation warnings?”
- “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, not2025-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:
- 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
- HAL (Hypertext Application Language)
- How does HAL structure
_linksand_embedded? - What’s the difference between HAL and JSON-LD?
- Book Reference: “REST API Design Rulebook” Ch. 6 - Mark Masse
- How does HAL structure
Questions to Guide Your Design
Before implementing, think through these:
- Link Representation
- Should links be absolute URLs or relative paths?
- Do you include templated URLs (e.g.,
/orders/{id})?
- 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:
- “Explain HATEOAS and why it’s rarely implemented in practice.”
- “What’s the difference between HAL, JSON-LD, and Siren formats?”
- “How does HATEOAS enable true API versioning without version numbers?”
- “What are the performance implications of including links in every response?”
- “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
relattributes 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:
- 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
- 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:
- Migration Strategy
- Do you dual-write (to both old and new columns) during transition?
- How do you backfill old data into the new schema?
- 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
nameorfirst_name + last_nameas 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:
- “Explain the Expand/Contract pattern for database migrations.”
- “How would you split a column without API downtime?”
- “What’s the difference between a migration and a refactoring?”
- “How do you handle data inconsistencies during dual-write phases?”
- “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:
- 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
- AST (Abstract Syntax Tree) Diffing
- How do you compare two JSON/YAML structures semantically, not textually?
- What’s the difference between
git diffand semantic diffing? - Book Reference: “Designing Web APIs” Ch. 9 - Jin, Sahni, & Shevat
Questions to Guide Your Design
Before implementing, think through these:
- Breaking Change Detection
- Is renaming a field breaking? (Yes, even if the data type is the same)
- Is changing from
type: stringtotype: [string, null]breaking? (No, it’s more permissive)
- 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:
- “How would you automatically detect breaking changes in an OpenAPI spec?”
- “What’s the difference between backward-compatible and forward-compatible changes?”
- “Explain how semantic versioning applies to APIs.”
- “How would you handle a team that wants to make a breaking change without bumping the major version?”
- “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
pathssection, not justcomponents.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:
- 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
- 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:
- 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?
- 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:
- “Explain idempotency and why it’s critical for payment APIs.”
- “How would you implement idempotency keys in a distributed system?”
- “What’s the difference between idempotency and deduplication?”
- “How long should you cache idempotency keys?”
- “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
SETNXor 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:
- 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
- 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:
- 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)?
- 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):
- 2024-01-15: Split
nameintofirstName/lastName→ REVERSE: Combine intoname - 2023-06-20: Nested user data under
userkey → REVERSE: Flatten to root - 2022-06-01: Renamed
usernametoname→ REVERSE: Rename back tousername
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:
- “Explain how Stripe’s date-based API versioning works.”
- “What are the advantages and disadvantages compared to URI versioning?”
- “How would you implement a reversible transformation pipeline?”
- “What happens if a client requests a version from before your API existed?”
- “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-01header - 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:
- 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
- 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:
- Client Identification
- Do you use API keys, JWTs, or OAuth tokens?
- Where do you store the version mapping (database, Redis, config file)?
- 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:
- “What’s the difference between an API gateway and a load balancer?”
- “How would you implement per-client versioning in a gateway?”
- “What are the performance implications of looking up client metadata on every request?”
- “How would you handle a gradual rollout (e.g., 10% on v2, 90% on v1)?”
- “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:
- Contract-First: A 1000+ line OpenAPI spec.
- Versioned Gateway: Routing by API-Key/Pinned Version.
- Strategy Pattern: Clean, transformer-based versioning logic.
- Idempotency: Safe payment processing.
- Sunset Logic: Automated deprecation headers.
- 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
- “Walk me through your process for implementing a breaking change in a high-traffic API.”
- “How do you ensure data consistency between different API versions sharing one database?”
- “What are the trade-offs between ‘Contract-First’ and ‘Code-First’ development?”
- “How do you detect and prevent accidental breaking changes in a large team?”
- “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
PUTandDELETEcan 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.