API Design Best Practices for 2026
By Sabbir AI
I once worked with an API that returned HTTP 200 for errors with the actual error code buried in the response body. The same API used POST for everything, including fetching data. Resource names were inconsistent—sometimes plural, sometimes singular, sometimes just random words. Documentation? A 3-year-old Word doc that was 60% outdated.
Using that API was like trying to have a conversation with someone who speaks a different language and keeps changing the rules mid-sentence. It made me want to quit programming and become a farmer.
Good API design is one of those things that seems simple until you actually try to do it. Then you realize there are a million tiny decisions to make, and each one can either delight or infuriate your users (developers). So let's talk about how to design APIs that people will actually enjoy using—or at least not actively hate.
RESTful Principles (But Actually Understanding Them)
Everyone says "we built a RESTful API" but half the time it's just HTTP endpoints with JSON. Real REST has specific principles, and following them makes your API way more intuitive.
Use proper HTTP methods:
- GET - Retrieve data (should never modify anything)
- POST - Create new resources
- PUT - Replace an entire resource
- PATCH - Partially update a resource
- DELETE - Remove a resource
Don't do this:
POST /api/getUsers // WHY
POST /api/deleteUser/123 // STOP
GET /api/createUser?name=John // NO
Do this:
GET /api/users // Get all users
GET /api/users/123 // Get specific user
POST /api/users // Create user
PATCH /api/users/123 // Update user
DELETE /api/users/123 // Delete user
See how readable that is? You can guess what each endpoint does without reading documentation. That's the goal.
Use proper status codes:
- 200 - Success (GET, PATCH, PUT)
- 201 - Created (POST)
- 204 - Success with no content (DELETE)
- 400 - Bad request (client error)
- 401 - Unauthorized (not logged in)
- 403 - Forbidden (logged in, but no permission)
- 404 - Not found
- 500 - Server error
I've debugged too many issues caused by APIs returning 200 for errors. Don't be that person. If it's an error, use an error status code. Your future self will thank you when you're debugging at 3 AM.
Naming Conventions That Don't Suck
This seems trivial but inconsistent naming is one of the fastest ways to make developers hate your API.
Resource names should be:
- Plural nouns:
/users, not/user - Lowercase:
/blog-posts, not/BlogPosts - Kebab-case for URLs:
/user-profiles - camelCase for JSON:
{"firstName": "John"}
Pick a convention and stick to it religiously. Nothing is more annoying than an API that uses camelCase in some endpoints and snake_case in others.
Nested resources:
GET /users/123/posts // Get posts by user 123
POST /users/123/posts // Create post for user 123
GET /posts/456/comments // Get comments on post 456
But don't go too deep. /users/123/posts/456/comments/789/likes is ridiculous. If you need more than 2-3 levels, rethink your resource structure.
Versioning (Do It From Day One)
Here's a mistake I've seen multiple times: launching an API without versioning, then realizing you need to make breaking changes. Now what? You can't just change it—existing clients will break. So you hack together some backwards compatibility logic that makes everyone's life miserable.
Version your API from day one, even if you think you'll never need it. Future you will thank past you.
URL versioning (my preference):
GET /api/v1/users
GET /api/v2/users
Clear, explicit, and easy to route. You can run v1 and v2 simultaneously, gradually deprecate v1, and everyone's happy.
Header versioning (also fine):
GET /api/users
Accept: application/vnd.myapi.v2+json
Cleaner URLs, but less visible. Developers might not notice which version they're using.
When to increment versions:
- Removing or renaming fields in responses
- Changing required request parameters
- Changing response structure significantly
Don't increment for:
- Adding new optional fields to responses
- Adding new endpoints
- Adding new optional parameters
- Bug fixes
Error Handling That Actually Helps
Bad error message: {"error": "Invalid input"}
Okay, what input? Which field? What's wrong with it? This tells me nothing.
Good error message:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
]
}
}
Now I know exactly what to fix. This is the difference between a 5-minute fix and a 30-minute debugging session.
Error response structure I use:
{
"error": {
"code": "ERROR_CODE", // Machine-readable
"message": "Human-readable description", // For developers
"details": [], // Optional additional context
"requestId": "abc-123" // For support/debugging
}
}
The requestId is clutch for debugging. User reports an error? They send you the requestId, you grep your logs, instant context. Way better than "it didn't work yesterday afternoon sometime."
Pagination (Don't Return 100,000 Records)
I once called an API endpoint that returned 50MB of JSON because they didn't implement pagination. My request timed out, their server crashed. Nobody won.
Always paginate list endpoints. Always.
Offset-based pagination (simple, but has issues):
GET /api/posts?limit=20&offset=40
Problems: If items are added/deleted while paginating, you might skip or duplicate results.
Cursor-based pagination (better for real-time data):
GET /api/posts?limit=20&cursor=abc123
Return a cursor pointing to the next page. More complex to implement but handles changing data better.
My standard response format:
{
"data": [...],
"pagination": {
"total": 1000,
"page": 2,
"perPage": 20,
"totalPages": 50,
"nextCursor": "xyz789" // if using cursor pagination
}
}
Gives clients everything they need to build pagination UI.
Filtering, Sorting, and Searching
Don't make developers fetch all data and filter client-side. That's barbaric.
Filtering:
GET /api/users?status=active&role=admin
GET /api/posts?author=123&published=true
Sorting:
GET /api/users?sort=createdAt:desc
GET /api/posts?sort=-createdAt // - prefix for descending
Searching:
GET /api/users?search=john
GET /api/posts?q=typescript
Document which fields are filterable, sortable, and searchable. Don't make developers guess.
Authentication and Security
Use HTTPS. Always. No exceptions. I don't care if it's "just a development API." Use HTTPS.
Authentication methods:
API Keys (simple, good for server-to-server):
GET /api/users
Authorization: Bearer api_key_here
Don't put API keys in URLs. They'll end up in logs, browser history, and analytics. Use headers.
OAuth 2.0 (better for user authentication):
Authorization: Bearer jwt_token_here
More complex setup but gives you proper user auth with scopes, token refresh, etc.
Rate limiting (prevent abuse):
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640000000
Return rate limit info in response headers so clients know when they're about to hit the limit.
Documentation (If It's Not Documented, It Doesn't Exist)
I cannot stress this enough: DOCUMENT YOUR API. A well-designed API with no docs is useless. A mediocre API with great docs is actually usable.
Use OpenAPI/Swagger:
Write your API spec in OpenAPI format. You get:
- Interactive documentation (developers can test endpoints directly)
- Auto-generated client libraries
- Contract testing
- Always up-to-date docs (if you do it right)
What to document for each endpoint:
- What it does (in plain English)
- Request method and URL
- Authentication required?
- Request parameters (type, required/optional, description, example)
- Request body schema
- Response schema for each status code
- Example request and response
- Possible errors
I like Swagger UI for interactive docs and Postman collections for example requests. Give developers multiple ways to explore your API.
Field Selection (Don't Return Everything)
Sometimes clients only need a few fields. Don't force them to download everything.
GET /api/users?fields=id,name,email
Returns only the requested fields. Saves bandwidth, speeds up responses, makes mobile apps happy.
GraphQL does this really well, which is why some people prefer it over REST. But you can add field selection to REST too.
Webhooks (Let Users Know When Stuff Happens)
Polling is inefficient. "Hey, did anything change? No? Okay. Hey, did anything change NOW? No? Okay..." Repeat 1000 times.
Webhooks are better: "Hey, register this URL and I'll tell you when something happens."
POST /api/webhooks
{
"url": "https://your-app.com/webhook",
"events": ["user.created", "payment.completed"]
}
When those events happen, you POST to their URL with the data. They don't need to poll. Everyone's happy, servers save resources.
Webhook best practices:
- Sign webhook payloads so receivers can verify they're from you
- Retry failed webhooks with exponential backoff
- Provide a webhook testing tool in your dashboard
- Document the exact payload format for each event
Caching and Performance
Use HTTP caching headers. Don't make clients re-fetch data that hasn't changed.
Cache-Control: max-age=3600 // Cache for 1 hour
ETag: "abc123" // Identifier for this version of the resource
Client can send If-None-Match: "abc123" on next request. If nothing changed, you return 304 Not Modified with no body. Saves bandwidth and processing.
For expensive operations:
Consider async processing. Don't make clients wait 30 seconds for a response.
POST /api/reports
{
"type": "sales",
"dateRange": "2026-01-01 to 2026-01-31"
}
Response: 202 Accepted
{
"jobId": "job-123",
"status": "processing",
"statusUrl": "/api/reports/job-123"
}
Client can poll the status URL or you can webhook them when it's done.
Testing Your API
Write integration tests for your API. Test the actual HTTP endpoints, not just the internal functions.
describe('POST /api/users', () => {
it('should create a user with valid data', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'John', email: 'john@example.com' })
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('John');
});
it('should return 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({ name: 'John', email: 'invalid' })
.expect(400);
});
});
Test happy paths AND error cases. An API that fails gracefully is almost as important as one that succeeds.
Deprecation Strategy
Eventually you'll need to remove old endpoints. Don't just delete them—that breaks everyone's code and makes people angry.
Deprecation process:
1. Announce deprecation (blog post, email, changelog)
2. Add deprecation header to old endpoints:
Deprecation: true
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
Link: ; rel="successor-version"
3. Give users 6-12 months to migrate
4. Start returning 410 Gone instead of working responses
5. Eventually remove the endpoint
Be generous with deprecation timelines. Developers have other priorities. Give them time.
Real Talk: What Actually Matters
You can't implement every best practice from day one. Here's what to prioritize:
Must have:
- Consistent naming conventions
- Proper HTTP methods and status codes
- Basic documentation
- Error messages that help developers
- HTTPS
Should have:
- Versioning
- Pagination
- Rate limiting
- OpenAPI docs
Nice to have:
- Field selection
- Webhooks
- Advanced caching
- GraphQL endpoint
Start with the must-haves. Add the rest as you grow. A simple, well-documented API beats a feature-rich, confusing one every time.
Final Thoughts
Good API design is about empathy. Put yourself in the shoes of the developer who'll use your API. What would make their life easier? What would frustrate them?
Every design decision should optimize for developer experience. Clear naming over clever naming. Helpful errors over terse errors. Comprehensive docs over "figure it out yourself."
Because at the end of the day, an API is a product. Your users are developers. Make their lives better and they'll love you for it. Make their lives harder and they'll complain on Twitter and Reddit about how much your API sucks.
Choose wisely.