DEV Community

Rizwan Saleem
Rizwan Saleem

Posted on

How to design APIs that developers love to use

How to design APIs that developers love to use

Practical API Design: REST, GraphQL, and gRPC

Choose your API style based on requirements, not hype. REST is the safe default for public APIs, gRPC wins on performance for internal microservices, and GraphQL excels when clients need flexible, efficient data retrieval.

Protocol Tradeoffs

Dimension REST GraphQL gRPC
Transport HTTP/1.1 HTTP/1.1 or HTTP/2 HTTP/2
Serialization JSON (typically) JSON Protocol Buffers (binary)
Performance Slower; transfers excess data Faster for clients; only requested data Fastest; compact payloads + multiplexing
Best Use Case Public APIs, simple web apps Complex frontend data needs Internal microservices, real-time streaming
Learning Curve Low Medium (new concepts/syntax) Medium (.proto files)
Overhead Low for simple cases Higher for simple queries Low after setup

Rule of thumb:

  • REST: Great for simplicity and public APIs
  • GraphQL: Use sparingly, mainly for frontend-driven projects with complex data needs
  • gRPC: Best for high-performance, internal, or streaming APIs

Naming Conventions

Consistent naming makes APIs predictable and discoverable:

  • Use plural nouns for collections: /users, /projects
  • Use nouns, not verbs, in URLs: GET /users not GET /getUsers
  • Lowercase with hyphens: /user-profiles, not /UserProfiles
  • Nested resources show hierarchy: /users/{id}/projects
  • Avoid verbs in paths; use HTTP methods for actions:
    • GET (retrieve), POST (create), PUT (replace), PATCH (partial update), DELETE

Example:

GET    /v1/users              # List users
GET    /v1/users/{id}         # Get user
POST   /v1/users              # Create user
PATCH  /v1/users/{id}         # Update user fields
DELETE /v1/users/{id}         # Delete user
Enter fullscreen mode Exit fullscreen mode

Consistent Error Handling

Return structured, informative errors with appropriate HTTP status codes:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid email format",
    "details": [
      {"field": "email", "issue": "must be valid email address"}
    ],
    "request_id": "req_abc123"
  }
}
Enter fullscreen mode Exit fullscreen mode

HTTP Status Code Guidelines:
| Status | When to Use |
|--------|-------------|
| 200 | Successful GET, PUT, PATCH |
| 201 | Successful POST (resource created) |
| 204 | Successful DELETE (no content) |
| 400 | Bad request (validation errors) |
| 401 | Missing/invalid authentication |
| 403 | Authenticated but insufficient permissions |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
| 500 | Server error |

Always include a request_id for debugging and logging correlation.

Versioning Strategies

Version from day one; never release an unversioned API:

URI Versioning (recommended):

/v1/users
/v2/users
Enter fullscreen mode Exit fullscreen mode

Key rules:

  • Always include version in URI, even for v1
  • Use format /v{number} (e.g., /v1, /v2)
  • Use simple ordinal numbers; avoid v1.2.3 or semver
  • Return 404 if version is missing
  • Maintain at least one version back for migration time

When to create a new version:

  • ✅ Modifying existing resources in breaking ways (new mandatory field, field removal)
  • ✅ Introducing new resources clients must interact with
  • ✅ Creating new error types
  • ❌ Adding optional fields (clients can ignore them)
  • ❌ Adding new resources clients don't need to use

Support migration:

  • Maintain a changelog and migration guide
  • Use Deprecation headers to warn before removing endpoints

Pagination

Always paginate list endpoints to handle large datasets efficiently:

Cursor-based pagination (recommended for APIs):

// Request
GET /v1/users?limit=20&cursor=eyJpZCI6MTAwfQ

// Response
{
  "data": [...],
  "pagination": {
    "cursor": "eyJpZCI6MjAwfQ",
    "has_more": true,
    "limit": 20
  }
}
Enter fullscreen mode Exit fullscreen mode

Alternative: Offset-based (simpler but less scalable):

GET /v1/users?offset=0&limit=20
Enter fullscreen mode Exit fullscreen mode

Best practices:

  • Set reasonable default limits (e.g., 20-50)
  • Allow clients to specify limit within bounds
  • Return pagination metadata alongside data
  • Use cursor-based for unordered或不稳定 data (e.g., real-time feeds)

Authentication

Choose authentication based on your use case:

Method Best For Notes
API Keys Server-to-server, usage tracking Simple but keep secret
OAuth 2.0 Delegated authorization Users grant limited access
JWT Stateless authentication Compact, self-contained tokens; often paired with OAuth 2.0
mTLS Service-to-service, high security Both client and server verify certificates

Authentication Pattern (JWT + OAuth 2.0):

Authorization: Bearer <token>
Enter fullscreen mode Exit fullscreen mode
  1. Extract token from Authorization header
  2. Verify signature and expiration
  3. Load user from token payload
  4. Attach user to request context
  5. Return 401 for invalid/missing tokens

Authorization:

  • Check roles/permissions after authentication
  • Use middleware/decorators for role checks
  • Return 403 for insufficient permissions

Documenting APIs

Document the contract first, before implementation:

Protocol Documentation Standard
REST OpenAPI (Swagger)
gRPC .proto files
GraphQL SDL (Schema Definition Language)

Essential documentation elements:

  • Endpoint URLs and HTTP methods
  • Request/response schemas with examples
  • Authentication requirements
  • Error codes and meanings
  • Rate limits
  • Versioning and deprecation notices

Use interactive docs (e.g., Swagger UI, Redoc) for better developer experience.

Designing for Evolvability

Keep interfaces stable and versioned to avoid breaking clients:

Key principles:

  • Favor simple, orthogonal operations: Each endpoint does one thing well; complex workflows belong in orchestrators
  • Plan for partial failure: Design retry policies, timeouts, and graceful degradation
  • Make data transfer efficient: Reduce payload size, use pagination, support field selection
  • Be liberal in what you accept, conservative in what you send (Postel's Law)
  • Add fields, don't remove them: Clients can ignore new optional fields
  • Deprecate before removing: Use Deprecation headers and announce timelines

Field selection (GraphQL-style flexibility in REST):

GET /v1/users?fields=id,name,email
Enter fullscreen mode Exit fullscreen mode

Real-World Patterns

Pattern 1: Filtering, Sorting, and Pagination Combined

GET /v1/users?status=active&sort=-created_at&limit=20&cursor=xyz
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Bulk Operations

POST /v1/users/bulk
{
  "operations": [
    {"action": "create", "data": {...}},
    {"action": "update", "id": "123", "data": {...}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Asynchronous Long-Running Jobs

POST /v1/reports
→ 202 Accepted
{ "job_id": "job_xyz", "status_url": "/v1/jobs/job_xyz" }

GET /v1/jobs/job_xyz
→ { "status": "completed", "result_url": "/v1/reports/abc" }
Enter fullscreen mode Exit fullscreen mode

Pattern 4: Conditional Requests (ETags)

GET /v1/users/123
→ ETag: "abc123"

PUT /v1/users/123
If-Match: "abc123"
→ Prevents lost updates
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  1. Pick based on requirements: REST for public APIs, gRPC for internal performance, GraphQL for frontend flexibility
  2. Version from day one using URI versioning (/v1/)
  3. Use consistent error format with request_id, error codes, and field-level details
  4. Always paginate list endpoints with cursor-based pagination for scalability
  5. Document with OpenAPI/.proto/SDL and make docs interactive
  6. Design for evolvability: add fields, deprecate carefully, maintain backward compatibility

Ship it with confidence by planning your API contract, versioning strategy, and error handling before writing code.


Rizwan Saleem — https://rizwansaleem.co

Top comments (0)