Friday, March 13, 2026

Why the Old Swagger Petstore Is Teaching You Bad API Design

Why the Old Swagger Petstore Is Teaching You Bad API Design

For over a decade, developers learning OpenAPI have started with the same example: the Swagger Petstore. It's been the go-to tutorial for understanding API specifications. But here's the problem—this widely-used example teaches anti-patterns that developers carry into production systems.

The old Petstore doesn't follow basic RESTful design principles. If you learned API design from it, you might be building APIs the wrong way.

The Three Critical Violations

1. Inconsistent Resource Naming

The old Petstore mixes singular and plural resource names:

GET /pet/{id}           ← Singular
GET /store/inventory    ← Plural
POST /user              ← Singular

This inconsistency creates confusion. Should you use /pet/123 or /pets/123? The answer: always use plural for collections.

Here's why plural wins:

Consistency across operations. When you use /pets, it works for both collections and individual resources: - GET /pets returns all pets - GET /pets/123 returns one pet - POST /pets creates a pet - PUT /pets/123 updates a pet

Clearer semantics. /pets/123 reads as "pet 123 from the pets collection." /pet/123 reads awkwardly.

Industry standard. GitHub uses /repos, Stripe uses /customers, Twitter uses /tweets. The pattern is established.

The Modern PetStore API fixes this:

GET /pets/{id}
GET /orders/{id}
GET /users/{id}

Every resource uses plural naming. No exceptions.

2. Action Verbs in URLs

The old Petstore includes endpoints like:

GET /pet/findByStatus?status=available
GET /pet/findByTags?tags=tag1,tag2

This violates a core REST principle: URLs should represent resources (nouns), not actions (verbs).

The HTTP method already tells you the action: - GET = retrieve - POST = create - PUT = update - DELETE = remove

Adding "find" to the URL is redundant. Worse, it creates inconsistency. Why is it /pet/findByStatus but not /pet/getById?

The correct approach uses query parameters:

GET /pets?status=available
GET /pets?tags=tag1,tag2

For complex searches that don't fit query parameters, use the QUERY method:

QUERY /pets/search
Content-Type: application/json

{
  "filters": {
    "status": "AVAILABLE",
    "breed": "Golden Retriever",
    "age": {"min": 1, "max": 5}
  }
}

The QUERY method (RFC 9535) is designed for complex queries that need a request body. It's semantically correct and avoids URL bloat.

3. Wrong HTTP Status Codes

The old Petstore returns incorrect status codes:

POST /pet
Response: 200 OK

DELETE /pet/{id}
Response: 200 OK with body

Both are wrong.

Creating resources should return 201 Created, not 200 OK. The 201 status tells clients a new resource exists and includes its location in the Location header:

POST /pets
Response: 201 Created
Location: /pets/019b4132-70aa-764f-b315-e2803d882a24

{
  "id": "019b4132-70aa-764f-b315-e2803d882a24",
  "name": "Max",
  "status": "AVAILABLE"
}

Deleting resources should return 204 No Content, not 200 OK. The 204 status indicates success without a response body. Returning 200 with a body wastes bandwidth and creates confusion—what should the body contain?

DELETE /pets/019b4132-70aa-764f-b315-e2803d882a24
Response: 204 No Content

The Modern PetStore API uses correct status codes throughout: - 200 OK for successful GET/PUT with body - 201 Created for successful POST - 204 No Content for successful DELETE - 400 Bad Request for client errors - 422 Unprocessable Entity for validation errors - 500 Internal Server Error for server failures

What Else Is Missing?

Beyond the three critical violations, the old Petstore lacks features you need in production:

No Standard Error Format

The old Petstore returns generic error messages with no structure. Modern APIs use RFC 9457 Problem Details:

{
  "type": "https://petstoreapi.com/errors/validation-error",
  "title": "Validation Error",
  "status": 422,
  "detail": "The request body contains validation errors",
  "instance": "/v1/pets",
  "errors": [
    {
      "field": "name",
      "message": "Name is required"
    }
  ]
}

This format is standardized, machine-readable, and includes enough context for debugging.

No Pagination

The old Petstore returns all results in one response. This doesn't scale. What happens when you have 10,000 pets?

Modern APIs use cursor-based pagination:

GET /pets?limit=20&cursor=eyJpZCI6IjAxOWI0MTMyIn0

Response:
{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6IjAxOWI0MTUzIn0",
    "hasMore": true
  }
}

Cursor pagination scales to millions of records without performance degradation.

No Rate Limiting

Production APIs need rate limiting to prevent abuse. The Modern PetStore API includes standard rate limit headers:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1678886400

Clients can adjust their behavior before hitting limits.

No Versioning Strategy

The old Petstore doesn't address versioning. How do you introduce breaking changes?

Modern APIs version through the URL:

GET /v1/pets/{id}
GET /v2/pets/{id}

Or through headers:

GET /pets/{id}
Accept: application/vnd.petstore.v2+json

Both approaches work. Pick one and stick with it.

No Security Model

The old Petstore has minimal security examples. Modern APIs need:

OAuth 2.0 with scopes:

security:
  - oauth2:
    - read:pets
    - write:pets

API key authentication:

Authorization: Bearer sk_live_abc123...

Rate limiting per user:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999

Security isn't optional in production.

How Modern PetStore API Fixes Everything

The Modern PetStore API was built to demonstrate correct API design:

RESTful URLs

GET    /pets              ← List all pets
POST   /pets              ← Create a pet
GET    /pets/{id}         ← Get one pet
PUT    /pets/{id}         ← Update a pet
DELETE /pets/{id}         ← Delete a pet
GET    /pets?status=AVAILABLE  ← Filter pets

Every URL follows REST principles. No action verbs. Consistent plural naming.

Correct Status Codes

  • 200 OK for successful reads and updates
  • 201 Created for successful creates
  • 204 No Content for successful deletes
  • 400 Bad Request for malformed requests
  • 401 Unauthorized for missing auth
  • 403 Forbidden for insufficient permissions
  • 404 Not Found for missing resources
  • 422 Unprocessable Entity for validation errors
  • 429 Too Many Requests for rate limiting
  • 500 Internal Server Error for server failures

Standard Error Format (RFC 9457)

{
  "type": "https://petstoreapi.com/errors/not-found",
  "title": "Resource Not Found",
  "status": 404,
  "detail": "Pet with ID 019b4132-70aa-764f-b315-e2803d882a24 not found",
  "instance": "/v1/pets/019b4132-70aa-764f-b315-e2803d882a24"
}

Cursor-Based Pagination

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6IjAxOWI0MTUzIn0",
    "prevCursor": "eyJpZCI6IjAxOWI0MTMyIn0",
    "hasMore": true
  }
}

OAuth 2.0 Security

components:
  securitySchemes:
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://auth.petstoreapi.com/oauth/authorize
          tokenUrl: https://auth.petstoreapi.com/oauth/token
          scopes:
            read:pets: View pet information
            write:pets: Manage pets

Rate Limiting

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1678886400
Retry-After: 60

What You Should Do

If you're building a new API:

  1. Use plural resource names (/pets, not /pet)
  2. Keep actions in HTTP methods (GET, POST, PUT, DELETE)
  3. Return correct status codes (201 for creates, 204 for deletes)
  4. Implement RFC 9457 error format
  5. Add cursor-based pagination
  6. Include rate limiting headers
  7. Version your API (URL or header-based)
  8. Use OAuth 2.0 or API keys for authentication

If you're maintaining an existing API:

  1. Audit your endpoints for RESTful violations
  2. Document breaking changes in your changelog
  3. Version new endpoints that fix design issues
  4. Deprecate old endpoints with sunset headers
  5. Migrate clients gradually to new patterns

Don't break existing clients, but stop propagating bad patterns.

The Bigger Picture

The old Swagger Petstore served its purpose as a simple teaching tool. But "simple" became "simplistic." It taught a generation of developers that REST is just HTTP + JSON.

REST is more than that. It's a set of architectural constraints that make APIs scalable, maintainable, and predictable.

The Modern PetStore API shows what's possible when you apply those constraints correctly. It's not just an example—it's a reference implementation you can learn from and build upon.

Your API is your product's interface to the world. Design it well.

Key Takeaways

  • The old Swagger Petstore violates basic RESTful principles
  • Use plural resource names consistently (/pets, not /pet)
  • Keep action verbs out of URLs—use HTTP methods instead
  • Return correct status codes (201 for creates, 204 for deletes)
  • Implement standard error formats (RFC 9457)
  • Add pagination, rate limiting, and proper security
  • The Modern PetStore API demonstrates correct patterns


from Anakin Blog http://anakin.ai/blog/swagger-petstore-teaches-bad-design/
via IFTTT

No comments:

Post a Comment

Stop Using Action Verbs in REST API URLs

You've seen URLs like these: GET /findUsersByEmail?email=user@example.com POST /createOrder GET /searchProducts?query=laptop DELETE /r...