22 min read

API Design Guidelines

Technical guidelines for designing high-quality, maintainable, and performant REST APIs.

API Design Guidelines

Designing APIs That Developers Actually Want to Use

I've spent the last several years helping teams build APIs for Azure, and I've seen firsthand how design choices directly impact developer productivity and happiness. The best APIs feel like they were built by someone who understands the customer's problems and genuinely wants to help solve them. The worst ones have left me wondering if anyone ever tried to use them before attempting to ship them.

This guide focuses on practical, developer-centered API design principles drawn from real-world experience. Rather than rehashing theoretical REST principles or academic design patterns, I'm sharing proven approaches that make APIs easier to work with, dramatically reducing the friction developers face during integration.

Throughout these guidelines, we'll focus on a simple question: "How will this design decision affect the developers who need to integrate with our API?" When we keep developers' needs at the center of our design process, we create APIs that become valuable tools rather than frustrating obstacles.

Looking for guidance beyond technical design? Check out the API Strategy and Governance Guide for organizational aspects, or the API Lifecycle Management Guide for implementation guidance throughout your API's lifecycle.

NOTE: These guidelines are intentionally generic and not tied to any specific cloud service implementation. I've noted where Azure does things a specific way where applicable, mainly for my own reference.

Developer-Centered Design Principles

Before diving into specific technical guidelines, let's establish some fundamental principles that center the developer experience in your API design process.

Put Developer Experience First with API-First Design

Too often, APIs are treated as an afterthought, a thin layer hastily added over existing systems. This approach inevitably creates frustrating developer experiences as the API inherits all the quirks and limitations of those systems.

Instead, think of your API as a user interface for your product that developers choose to use (or not use). When you design your API before implementation:

  • You focus on what makes sense for developers, not what's convenient for your systems
  • You gather feedback on your design when it's still relatively cheap to make changes
  • You create a contract that both API consumers and implementers can work with confidently

Consider using tools like TypeSpec to formalize your API contract. This helps ensure your design is complete and consistent before a single line of implementation code is written.

Make Your API Easy to Learn Through Consistency

Consistency might be the single most important factor in API usability. Each inconsistency forces developers to pause, check documentation, and mentally context-switch. When your API follows consistent patterns:

  • Developers can learn a small part of your API and accurately predict how the rest works
  • They spend less time reading documentation and more time building valuable features
  • Their code becomes more consistent and maintainable

Apply uniform patterns for everything from HTTP status codes to query parameters to error responses. When developers can trust that your API behaves predictably, they develop faster and with more confidence.

Reduce Cognitive Load with Thoughtful Simplicity

Complex APIs create cognitive burden for developers. Every parameter, option, header, and response field they need to understand takes mental energy away from solving their actual problems.

The best APIs hide complexity rather than expose it. Ask yourself:

  • "Is this the simplest possible design that meets real developer needs?"
  • "Am I exposing implementation details that developers shouldn't need to worry about?"
  • "Could I make common operations simpler while keeping advanced capabilities available?"

Remember that different API audiences have different needs. An internal API for your own microservices might expose different capabilities than a partner or public API.

Respect Developer Investments by Designing for Evolution

Your API will inevitably change over time. How you manage that change directly impacts developers who have invested time and trust in your platform.

Planning for evolution from the beginning prevents painful breaking changes later:

  • Design resources to be extensible so you can add fields without breaking existing clients
  • Use versioning strategies that provide stability guarantees
  • Follow conventions like "must-ignore-unknown" so clients don't break when you add capabilities

Developers build businesses and careers on your API. Respect that investment by making your API stable and predictable as it evolves.

Practical API Design Guidelines

Now let's explore specific guidelines organized by different aspects of API design.

Creating a Consistent Experience Through Naming

The names you choose for your API resources, fields, and operations create the vocabulary developers will use to understand your service. Thoughtful naming makes your API more intuitive and reduces the learning curve.

Connect to Your Business Domain

Use domain-driven naming that reflects the business context. Your API names should map to business capabilities rather than implementation details. Ask yourself, "Would a business stakeholder recognize these terms?" This approach:

  • Makes your API more intuitive to understand
  • Creates alignment between technical and business teams
  • Avoids exposing internal implementation details that might change

Standardize Your Naming Patterns

Maintaining consistent casing and plurality patterns makes your API feel cohesive:

  • Use singular nouns for individual resources and plural for collections
  • Apply either camelCase or snake_case consistently across all services
  • Be consistent with verb usage in operation names

Create a Shared Vocabulary

Consider establishing a glossary of approved terms and concepts. This standardizes terminology across APIs and prevents the cognitive load that comes from having multiple terms for the same concept. Whenever possible, align your API vocabulary with industry-standard terminology to leverage existing developer knowledge.

Modeling Resources That Make Sense

How you structure and represent your API resources significantly impacts both usability and performance.

Focus on Business Entities, Not Tables

Model resources based on business entities, not database tables. Your public contract should hide implementation details and represent meaningful business objects with clear lifecycles. This approach:

  • Creates more stable APIs that can survive implementation changes
  • Produces more intuitive resources that align with how developers think
  • Allows for better separation of concerns between API and implementation

Find the Right Balance of Data Inclusion

Carefully consider the tradeoff between normalization and denormalization. While normalized data (following database principles with minimal redundancy) can be efficient for storage, it often creates poor API experiences by requiring multiple calls.

Instead, optimize representations for consumption rather than storage. Consider embedding frequently accessed related data to reduce round trips. For example, a "project" resource might include basic information about team members rather than just their IDs.

Build in Room for Growth

Design for evolvability through explicit extension points. For example:

  • Include capability discovery mechanisms
  • Consider using properties objects for logical groupings that can expand
  • Follow the "must-ignore-unknown" principle so clients don't break when you add fields

Crafting Intuitive URLs

Your URL structure serves as both a navigational system and a conceptual model of your API.

Structure Paths Logically

Create hierarchical paths that reflect real resource relationships. Express ownership and containment through path structure, but limit path depth to maintain understandability. For example:

/projects/{projectId}/tasks/{taskId}

This clearly communicates that tasks belong to projects.

Use Path Parameters Appropriately

Reserve path parameters for resource identity, and use query parameters for filtering, sorting, and pagination. For example:

/projects/{projectId}          # Good: identity in path
/projects?name=Homepage        # Good: filtering in query
/projects/findByName/Homepage  # Avoid: filtering in path

Standardize Collection Handling

Establish conventions for collection pagination and filtering that remain consistent across your entire API surface. This consistency makes your API more predictable and easier to use programmatically.

Using HTTP Methods Meaningfully

HTTP methods carry semantic meaning that can make your API more intuitive when used correctly.

Follow Standard Method Semantics

Apply HTTP methods according to their standard meanings:

  • GET for retrieval (never for changing state)
  • POST for creation or complex operations
  • PUT for replacement of an entire resource
  • PATCH for partial updates
  • DELETE for removal

Design for Idempotency

Ensure appropriate operations are idempotent (producing the same result regardless of how many times they're called). This quality is crucial for reliable APIs that can handle retries after network failures.

GET, PUT, and DELETE should always be idempotent. POST operations aren't naturally idempotent but can be made so with client-generated IDs or operation IDs.

Support Efficient Bulk Operations

For common operations that might need to handle multiple resources at once, consider adding bulk endpoints. These can dramatically improve performance and reduce network overhead. When implementing bulk operations, be clear about atomicity guarantees. Will the operation succeed or fail as a whole, or can individual items fail independently?

Structuring Your URLs for Success

Let's take a deeper look at URL design since it's often the first thing developers notice about your API.

Resource-Oriented Design Principles

When structuring your URLs, think about resources (nouns) rather than actions (verbs). This creates a more consistent, RESTful API that's easier to understand:

  • Use nouns for resources: /projects, /tasks (not verbs like /getProjects)
  • Use plural nouns for collections: /projects (not /project)
  • Nest resources logically: /projects/{projectId}/collaborators

Creating a Clear URL Hierarchy

The structure of your URLs creates a mental model of your API:

  • Keep URLs short and intuitive
  • Limit nesting to 2-3 levels: /resource/{id}/subresource/{subId}
  • Use query parameters for filtering: /tasks?projectId=123&isCompleted=false

Identifying Resources Consistently

How you identify resources affects both security and usability:

  • Use consistent identifiers (UUIDs or stable strings)
  • Avoid exposing raw database IDs when possible for security reasons
  • Consider human-readable slugs for resources where appropriate

Making the Most of HTTP Methods

Understanding how to leverage HTTP methods properly can make your API more intuitive and reliable.

Standard HTTP Method Usage

Each HTTP method has a specific purpose and properties:

Method Purpose Properties
GET Retrieve resources Safe, idempotent
POST Create resources or trigger operations Not naturally idempotent¹
PUT Create or replace resources entirely Idempotent
PATCH Update resources partially Idempotent²
DELETE Remove resources Idempotent

¹ Can be made idempotent with client-generated IDs or operation IDs
² When using JSON Merge Patch or similar approach

Note: All operations should be designed to be retriable for fault tolerance against network failures. This is a crucial aspect of building reliable APIs.

Understanding Method Properties

Safe methods don't change state:

  • GET, HEAD, OPTIONS, TRACE

Idempotent methods produce the same result regardless of repetition:

  • GET, HEAD, PUT, DELETE, OPTIONS, TRACE
  • PATCH (when properly implemented)

This property is essential for reliable APIs since it allows clients to retry operations safely after network failures.

Creating Clear Request and Response Formats

The structure of your request and response payloads directly impacts developer experience.

Choosing Data Formats

  • Use JSON as your primary exchange format (it's widely supported and human-readable)
  • Apply consistent property naming (camelCase or snake_case)
  • Include appropriate Content-Type headers: Content-Type: application/json

Designing Request Bodies

  • Include request bodies only with POST, PUT, and PATCH
  • Validate all inputs with clear error messages
  • Clearly distinguish required from optional fields

Remember that required/optional status may vary by operation type. A field might be required for creation but optional for updates.

Structuring Responses Clearly

  • Use appropriate HTTP status codes
  • Return the resource or collection in the response body
  • For collections, consider including metadata:
{
  "items": [...],
  "totalCount": 100,
  "nextLink": "https://api.example.com/resources?skip=50&top=10"
}

Note that including total counts may impact performance with large datasets and some storage technologies. Consider making this optional or paginating large collections.

Turning Errors into Developer Guidance

Few things frustrate developers more than unhelpful error messages. Errors are inevitable, but they don't have to be dead ends. Well-designed error responses transform moments of frustration into clear paths forward.

Status Codes as the First Line of Communication

HTTP status codes provide the first clue about what went wrong. Using them consistently and appropriately helps developers quickly understand the general category of an issue:

Code Description What It Tells Developers
200 OK Success Their request worked as expected
201 Created Resource created Their creation request succeeded and the resource exists
204 No Content Success with empty response The operation succeeded but there's no content to return
400 Bad Request Invalid request format/parameters Something was wrong with their request syntax or parameters
401 Unauthorized Authentication required They need to authenticate (or re-authenticate)
403 Forbidden Authentication succeeded but permissions lacking They don't have permission for this operation
404 Not Found Resource not found The resource they're looking for doesn't exist
409 Conflict Request conflicts with resource state The operation can't be completed due to the current state
422 Unprocessable Entity Valid syntax but semantic errors The request was well-formed but had logical errors
429 Too Many Requests Rate limit exceeded They're making too many requests and need to slow down
500 Internal Server Error Server failure Something went wrong on your end, not theirs

Azure note: Some systems (like Azure) treat 200 and 201 as functionally identical for idempotency reasons. A client might create a resource (201) but then timeout waiting for the response and retry the operation. In this case, the resource was already created and so the operation returns 200.

Designing Error Responses That Guide Rather Than Frustrate

The status code is just the beginning. Detailed error bodies help developers understand exactly what went wrong and how to fix it. Use a consistent structure across all endpoints:

{
  "error": {
    "code": "invalid_parameter",
    "message": "One or more parameters in your request were invalid",
    "details": [
      {
        "code": "value_out_of_range",
        "target": "age",
        "message": "Age must be between 0 and 120"
      }
    ]
  }
}

Think about error responses from a developer's perspective:

  • What happened? Clear error codes let them programmatically handle specific error conditions
  • Why did it happen? Human-readable messages explain the issue in plain language
  • How can they fix it? The most helpful errors suggest specific remediation steps
  • How can they troubleshoot further? Request IDs correlate errors with your logs

Common Error Response Pitfalls to Avoid

Even well-intentioned error designs can create frustration. Watch out for these common mistakes:

  • Generic error messages that don't help identify the specific problem
  • System-oriented messages that leak implementation details ("Error in SQL query XYZ")
  • Inconsistent formats that force developers to handle different error structures
  • Missing validation context that doesn't tell them which field failed or why
  • Unhelpful remediation like "Contact support" for problems they could fix themselves

Remember that every error is a moment where developers decide whether your API is worth the trouble. Great error design turns these moments into opportunities to demonstrate quality and build trust.

Managing Large Sets of Data

APIs often need to handle large collections of data efficiently.

Implementing Effective Pagination

Most collection endpoints should implement pagination to:

  • Improve response times
  • Reduce server load
  • Avoid overwhelming clients with large datasets

Common pagination approaches include:

  • Using query parameters like skip and top (simple and effective)
  • Returning a "next link" URL for the client to retrieve the next page

Set sensible defaults and maximum page sizes to prevent abuse and ensure performance.

Supporting Filtering and Sorting

  • Use query parameters for filtering:

    ?status=active&priority=high
    
  • Use query parameters for sorting:

    ?sort=createdAt&order=desc
    

Some systems may use a domain-specific query language like:

?filter=status eq 'active' and priority eq 'high'

Whatever approach you choose, document all supported filter and sort options clearly.

Versioning with Developer Impact in Mind

API versioning might seem like a technical implementation detail, but it directly impacts the level of trust developers place in your platform. When a team builds on your API, they're making a significant investment. Your versioning strategy signals how much you respect that investment.

Understanding Versioning from the Developer's Perspective

From a developer's perspective, any change that breaks their code is a major pain point, regardless of how minor it might seem to you. It means:

  • Interrupted development of their own features
  • Time spent diagnosing what changed
  • Potential production outages if breaking changes weren't communicated well
  • Explaining to their own stakeholders why they need to rework something that was "already done"

With this context in mind, let's compare common versioning approaches:

Approach Example Developer Experience Pros Developer Experience Cons
URL path /v1/resources Easy to see which version they're using, simple to test different versions Changing versions means updating all endpoint URLs in their code
Custom header API-Version: 1 Cleaner URLs, can switch versions with a single header change Less visible in browser tools, easy to miss in documentation
Accept header Accept: application/vnd.company.v1+json Follows HTTP standards for content negotiation Unfamiliar to many developers, harder to test in browsers
Query parameter ?api-version=2023-01-01 Works in browsers, requires minimal client changes Can conflict with resource filtering, less semantically clear

While URL path versioning isn't perfect from a pure REST perspective, many developers prefer it because it's explicit and easy to understand. The best approach is the one that makes the most sense for your particular developer audience.

Building Developer Trust Through Versioning Promises

Your versioning strategy should come with clear promises that developers can rely on:

  • Explicitly defined support windows so they know how long each version will be maintained
  • Guarantees about what kinds of changes might be made within a version
  • Detailed release notes that highlight any changes that might affect them
  • Advance notice of upcoming versions and deprecations
  • Overlapping support periods that give them adequate time to migrate

When developers can count on these promises, they're more willing to adopt new versions of your API and trust your platform for critical applications.

Creating Migration Paths That Respect Developer Time

When you need to introduce a new version with breaking changes, show respect for developers' time and investment by:

  • Creating detailed migration guides with specific before/after examples
  • Providing tools that help identify usage of deprecated features
  • Implementing feature flags that allow incremental adoption
  • Considering a hybrid approach during transition periods
  • Testing both versions thoroughly to ensure all documented migration paths work correctly

Remember that forcing developers to rewrite working integrations creates frustration and erodes trust. The more painless you make version transitions, the more likely developers will stay with your platform long-term.

Performance Optimizations That Developers Will Thank You For

Performance isn't just a technical concern; it directly impacts developer experience and the applications they build on your API. From a developer's perspective, an API that performs poorly forces them to create complex workarounds, explain slowness to their users, or even abandon your platform entirely.

Making Response Optimization Developer-Friendly

Every unnecessary byte you send wastes bandwidth, increases parsing time, and slows down the developer's application. Consider these developer-friendly optimizations:

  • Support field selection so developers can request exactly what they need:

    /users/123?fields=name,email,role
    
  • Enable compression (gzip/deflate) to reduce payload size without any changes to their code

  • Implement conditional requests with ETags so developers can avoid retrieving unchanged data:

    // First request gets data and an ETag
    GET /resources/123
    Response: ETag: "a1b2c3"
    
    // Subsequent request only gets data if it changed
    GET /resources/123
    If-None-Match: "a1b2c3"
    Response: 304 Not Modified (if unchanged)
    
  • Use appropriate cache controls to help developers leverage HTTP caching

The best performance optimizations are those that developers can opt into based on their specific needs without adding complexity to the basic API experience.

Batch Operations That Respect Developer Workflows

Without batch capabilities, developers often resort to making dozens or hundreds of individual API calls, creating brittle code, hitting rate limits, and delivering a poor experience to their own users. Well-designed batch operations address these problems:

  • Provide intuitive batch endpoints that follow the same patterns as single-resource operations
  • Support reasonable batch sizes that balance between efficiency and server load
  • Design for consistent error handling so one bad item doesn't fail the entire batch
  • Return itemized responses that make it clear which items succeeded and which failed

When implementing batch operations, be explicit about atomicity guarantees. Developers need to know whether a batch is all-or-nothing or if partial success is possible, as this significantly impacts how they handle errors.

Performance Considerations Beyond Speed

Performance isn't just about raw speed. Developers also care about:

  • Predictable response times that don't unexpectedly slow down
  • Reliable rate limiting with clear headers showing limits and remaining quota
  • Graceful degradation during high load instead of random failures
  • Transparent performance characteristics documented for different endpoints and operations

Remember that performance issues compound as developers build more complex applications. What seems like an acceptable 300ms delay in your tests might become a critical bottleneck when combined with other operations in a real-world scenario.

Handling Special Data Types

Some data types require special consideration in API design.

Working with Dates and Times

  • Use RFC3339 format, with at most three digits of fractional seconds: YYYY-MM-DDTHH:mm:ss.sssZ
  • Store and transmit dates in UTC
  • Include timezone information when needed

Examples:

  • 2023-10-15T14:30:00Z (UTC)
  • 2023-10-15T10:30:00-04:00 (with timezone offset)

Best practices:

  • Distinguish between date-only and date-time fields
  • Use consistent naming: dueDate vs. createdAt
  • Document expected precision

For durations, consider:

  • Simple integer values with specified units (e.g., durationHours: 24)
  • ISO 8601 format: P1Y2M10DT2H30M (standard but has limitations)

For ranges, provide explicit start and end fields.

Managing File Operations

APIs often need to handle file uploads and downloads, which present unique challenges.

Designing File Upload Capabilities

  • For small files: Use multipart/form-data when combining binary data with other request fields
  • For binary-only requests: Content-Type can match the file type
  • For large files: Consider chunked upload capabilities

There's no fixed size threshold for chunking. The tradeoff involves network reliability - larger uploads have higher chances of network failure, requiring full retransmission. Services should allow clients to decide their chunk size based on their network conditions.

Implementing File Download Operations

  • For binary data, implement partial content (HTTP Range) support
  • Return appropriate Content-Type headers matching the file type
  • Include Content-Disposition headers when appropriate
  • Consider whether to implement image transformations via query parameters

Handling Real-Time Data

Let's face it, sometimes a request-response pattern just isn't enough. Developers often need data as it happens, not just when they manually ask for it.

Exploring Real-Time Options for Different Needs

When considering real-time data delivery, you're essentially choosing from a spectrum of approaches, each with different tradeoffs for developers:

Option Description Best for
Periodic polling Developer's code regularly checks for updates Simple implementation, universal support
Server-Sent Events (SSE) Server pushes updates over standard HTTP One-way notifications with minimal overhead
WebSockets Full bidirectional communication Interactive applications requiring two-way communication
Long Polling HTTP connection stays open until data is available Legacy fallback when WebSockets aren't available

The right choice depends heavily on what your developers are building. A dashboard displaying real-time metrics might work well with SSE, while a collaborative document editor would benefit from WebSockets.

Remember that each approach places different implementation burdens on your developers. Polling is universally understood but inefficient, while WebSockets offer the best experience but require more complex client implementations.

Designing Developer-Friendly Webhooks

Webhooks flip the traditional model by having your API call the developer's API when events happen. They're powerful for event-driven architectures, but they can be frustrating to work with if not designed thoughtfully.

When implementing webhooks, prioritize the developer experience by:

  • Allowing fine-grained subscription to specific events (not forcing them to filter irrelevant events)
  • Including comprehensive event metadata (type, timestamp, correlation IDs) that helps with debugging
  • Implementing retry policies with exponential backoff for failed deliveries
  • Providing a way to verify webhook authenticity (signatures or HMAC)
  • Creating a developer portal where they can inspect recent webhook attempts

Always remember that webhooks essentially transfer operational burden to your developers. Their systems now need to be available to receive your calls, handle duplicates gracefully, and process events reliably. Provide clear documentation on how to handle common challenges like duplicate events and out-of-order delivery.

Note that webhooks present challenges with firewalls, client reliability, and error handling. Services must handle various failure scenarios, including unresponsive clients and potential infinite loops. Consider webhooks only when necessary and implement proper safeguards.

Bringing It All Together: Your API as a Developer Product

Throughout this guide, we've explored how thoughtful API design directly impacts developer experience. When you prioritize developer needs in your design decisions, you're not just building an interface to your systems, you're building a product that developers willingly choose to invest their time and trust in.

The Developer Journey Shapes API Success

Every API design choice shapes the developer's journey:

  • Clear, consistent naming reduces the cognitive load of learning your API
  • Intuitive resource modeling helps developers build mental models that match their problem space
  • Thoughtful error messages transform frustrating dead-ends into solvable problems
  • Well-designed versioning builds trust that their integration won't unexpectedly break

The most successful APIs aren't just technically sound. They're a user interface designed with consideration for the developers who use them. They anticipate common use cases, provide clear paths forward when things go wrong, and respect the developer's time and expertise.

APIs as Contracts of Trust

Remember that your API represents a promise to developers. When a team chooses your API, they're betting part of their product's success on your reliability, consistency, and thoughtfulness. This creates a responsibility to design with their needs in mind from day one.

Great API design isn't about following theoretical rules. It's about creating interfaces that help developers solve real problems efficiently and confidently, with the least amount of friction possible. By focusing primarily on their experience, you establish your API as a trusted, reliable tool that developers can depend on for their critical projects and recommend to colleagues facing similar challenges.

Tags: