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

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
andtop
(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.