API Design Guidelines
Introduction
We build APIs that power our applications and integrate with external systems. This guide outlines our principles and best practices for designing consistent, maintainable, and developer-friendly APIs.
API Design Principles
1. API-First Development
We practice API-first development, meaning:
- Design the API contract before implementing the solution
- Consider the API consumer's perspective from the start
- Use the API specification as the source of truth for development
- Leverage tools like OpenAPI/Swagger for documentation
2. RESTful Design
Our APIs follow RESTful design principles where appropriate:
- Use appropriate HTTP methods for operations
- Design resource-oriented URLs
- Leverage HTTP status codes correctly
- Apply consistent URL patterns
3. Consistency
Consistency helps developers predict how our APIs behave:
- Follow the same patterns across all endpoints
- Use consistent naming conventions
- Apply uniform error handling
- Maintain consistent versioning strategies
4. Security By Design
Security is a fundamental aspect of our API design:
- Implement proper authentication and authorization
- Validate and sanitize all inputs
- Apply rate limiting where appropriate
- Follow the principle of least privilege
API URL Design
Resource Naming
- Use nouns to represent resources (e.g.,
/users, not/getUsers) - Use plural forms for collection resources (e.g.,
/products, not/product) - Use kebab-case for multi-word resource names (e.g.,
/order-items) - Avoid verbs in resource paths, except for special actions
Examples:
# Good
GET /users
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
# Special actions (when necessary)
POST /users/123/reset-password
POST /orders/456/cancel
Hierarchical Relationships
Express parent-child relationships in URLs:
GET /customers/{customerId}/orders
GET /orders/{orderId}/items
Query Parameters
Use query parameters for:
- Filtering:
GET /products?category=electronics&price_min=100 - Sorting:
GET /products?sort=price_asc - Pagination:
GET /products?limit=25&offset=50 - Search:
GET /products?q=smartphone
API Versioning
Use URL path versioning for major changes:
/api/v1/users
/api/v2/users
HTTP Methods
Use HTTP methods consistently according to their semantics:
| Method | Purpose | Example |
|---|---|---|
GET | Retrieve data without side effects | GET /products |
POST | Create new resources | POST /products |
PUT | Replace a resource entirely | PUT /products/123 |
PATCH | Update a resource partially | PATCH /products/123 |
DELETE | Remove a resource | DELETE /products/123 |
Request/Response Formats
Content Types
- Default to
application/jsonfor request and response bodies - Support
application/x-www-form-urlencodedwhere necessary - Use
multipart/form-datafor file uploads
JSON Formatting
- Use camelCase for property names
- Include a root element only when necessary
- Represent dates as ISO 8601 strings (
YYYY-MM-DDTHH:mm:ss.sssZ)
Example Response:
{
"id": 123,
"firstName": "John",
"lastName": "Doe",
"emailAddress": "john.doe@example.com",
"createdAt": "2025-01-15T14:30:00.000Z",
"addresses": [
{
"id": 456,
"type": "billing",
"street": "123 Main St",
"city": "Austin",
"state": "TX",
"postalCode": "78701"
}
]
}
HTTP Status Codes
Use standard HTTP status codes to indicate success or failure:
Success Codes
200 OK: Request succeeded201 Created: Resource created successfully204 No Content: Request succeeded, but no content to return
Client Error Codes
400 Bad Request: Malformed request or invalid data401 Unauthorized: Authentication required403 Forbidden: Authentication succeeded, but no permission404 Not Found: Resource not found422 Unprocessable Entity: Validation errors
Server Error Codes
500 Internal Server Error: Unexpected server error503 Service Unavailable: Server temporarily unavailable
Error Handling
Error Response Format
Use a consistent error format:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request contains invalid parameters",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "password",
"message": "Must be at least 8 characters"
}
]
}
}
Common Error Codes
Define application-specific error codes:
| Code | Description |
|---|---|
VALIDATION_ERROR | Input validation failed |
AUTHENTICATION_ERROR | Authentication failed |
AUTHORIZATION_ERROR | Permission denied |
RESOURCE_NOT_FOUND | Requested resource not found |
RESOURCE_CONFLICT | Resource already exists |
RATE_LIMIT_EXCEEDED | Too many requests |
Pagination
Limit-Offset Pagination
Use limit and offset parameters:
GET /products?limit=25&offset=50
Response includes pagination metadata:
{
"data": [/* items */],
"pagination": {
"limit": 25,
"offset": 50,
"total": 1500
}
}
Cursor-Based Pagination
For large collections, use cursor-based pagination:
GET /events?limit=25&after=eyJpZCI6MTAwfQ==
Response includes next cursor:
{
"data": [/* items */],
"pagination": {
"limit": 25,
"next": "eyJpZCI6MTI1fQ==",
"hasMore": true
}
}
Filtering and Sorting
Filtering
Use query parameters for filtering:
GET /products?category=electronics&price_min=100&price_max=500&in_stock=true
Sorting
Use the sort parameter with field and direction:
GET /products?sort=price_asc,name_asc
Authentication and Authorization
Authentication
- Use OAuth 2.0 or JWT for authentication
- Include token in Authorization header:
Authorization: Bearer <token> - Return
401 Unauthorizedfor invalid or missing tokens
Authorization
- Check permissions after authentication
- Return
403 Forbiddenfor insufficient permissions - Document required permissions for each endpoint
Rate Limiting
Implement rate limiting to protect APIs:
- Include rate limit headers in responses:
X-Rate-Limit-Limit: 100
X-Rate-Limit-Remaining: 95
X-Rate-Limit-Reset: 1614556800 - Return
429 Too Many Requestswhen limit exceeded
Caching
Support caching where appropriate:
- Use
ETagandIf-None-Matchheaders - Return
304 Not Modifiedwhen content unchanged - Set appropriate
Cache-Controlheaders:Cache-Control: max-age=3600, must-revalidate
Batch Operations
Support batch operations for efficiency:
// POST /api/v1/batch
{
"operations": [
{
"method": "GET",
"path": "/users/123"
},
{
"method": "POST",
"path": "/products",
"body": {
"name": "New Product",
"price": 29.99
}
}
]
}
Documentation
API Documentation Standards
- Provide OpenAPI/Swagger documentation for all APIs
- Include complete endpoint descriptions
- Document all parameters, request/response formats, and status codes
- Document authentication requirements
Example Documentation Structure
openapi: 3.0.0
info:
title: Kirana Labs API
version: '1.0'
paths:
/users:
get:
summary: List users
description: Retrieve a list of users with pagination and filtering
parameters:
- name: limit
in: query
description: Maximum number of users to return
schema:
type: integer
default: 25
# More parameters...
responses:
'200':
description: A list of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
# More responses...
NextJS API Implementation
API Structure with App Router
/app/api/[resource]/route.ts
Example implementation:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
// Input validation schema
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user'),
});
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit') || '25');
const offset = parseInt(searchParams.get('offset') || '0');
try {
const users = await db.user.findMany({
take: limit,
skip: offset,
orderBy: { createdAt: 'desc' },
});
const count = await db.user.count();
return NextResponse.json({
data: users,
pagination: {
limit,
offset,
total: count,
},
});
} catch (error) {
console.error('Failed to fetch users:', error);
return NextResponse.json(
{
error: {
code: 'DATABASE_ERROR',
message: 'Failed to fetch users',
},
},
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate input
const validation = userSchema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid user data',
details: validation.error.format(),
},
},
{ status: 422 }
);
}
const userData = validation.data;
// Check for existing user
const existingUser = await db.user.findUnique({
where: { email: userData.email },
});
if (existingUser) {
return NextResponse.json(
{
error: {
code: 'RESOURCE_CONFLICT',
message: 'User with this email already exists',
},
},
{ status: 409 }
);
}
// Create user
const user = await db.user.create({
data: userData,
});
return NextResponse.json(user, { status: 201 });
} catch (error) {
console.error('Failed to create user:', error);
return NextResponse.json(
{
error: {
code: 'DATABASE_ERROR',
message: 'Failed to create user',
},
},
{ status: 500 }
);
}
}
Testing APIs
Unit Testing
// app/api/users/route.test.ts
import { NextRequest } from 'next/server';
import { GET, POST } from './route';
import { db } from '@/lib/db';
// Mock the database
jest.mock('@/lib/db', () => ({
user: {
findMany: jest.fn(),
count: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
},
}));
describe('Users API', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('GET', () => {
it('returns users with pagination', async () => {
const mockUsers = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
];
(db.user.findMany as jest.Mock).mockResolvedValue(mockUsers);
(db.user.count as jest.Mock).mockResolvedValue(10);
const request = new NextRequest('http://localhost:3000/api/users?limit=2&offset=0');
const response = await GET(request);
const result = await response.json();
expect(response.status).toBe(200);
expect(result.data).toEqual(mockUsers);
expect(result.pagination).toEqual({
limit: 2,
offset: 0,
total: 10,
});
});
});
describe('POST', () => {
it('creates a new user', async () => {
const userData = {
name: 'New User',
email: 'new@example.com',
role: 'user',
};
(db.user.findUnique as jest.Mock).mockResolvedValue(null);
(db.user.create as jest.Mock).mockResolvedValue({
id: 3,
...userData,
createdAt: new Date(),
});
const request = new NextRequest('http://localhost:3000/api/users', {
method: 'POST',
body: JSON.stringify(userData),
});
const response = await POST(request);
expect(response.status).toBe(201);
const result = await response.json();
expect(result).toHaveProperty('id', 3);
expect(result).toHaveProperty('name', 'New User');
});
it('returns validation errors', async () => {
const userData = {
name: 'A', // Too short
email: 'invalid-email', // Invalid email
};
const request = new NextRequest('http://localhost:3000/api/users', {
method: 'POST',
body: JSON.stringify(userData),
});
const response = await POST(request);
expect(response.status).toBe(422);
const result = await response.json();
expect(result.error).toHaveProperty('code', 'VALIDATION_ERROR');
});
});
});
API Security Checklist
✅ Authentication is required for protected resources
✅ Authorization checks are implemented
✅ Input data is properly validated
✅ Rate limiting is implemented
✅ HTTPS is enforced
✅ CORS is properly configured
✅ Security headers are set (X-Content-Type-Options, etc.)
✅ Sensitive data is never exposed in URLs
✅ Error messages don't leak sensitive information
✅ Database queries are protected against injection
Performance Considerations
- Implement pagination for large collections
- Consider filtering at the database level
- Use appropriate indexes for frequently queried fields
- Cache responses when appropriate
- Compress responses (gzip/Brotli)
- Implement database-level optimizations for complex queries
- Consider using Edge functions for geographical distribution
API Design Review Checklist
Before releasing a new API, ensure:
- API follows RESTful conventions
- Resource naming is consistent
- HTTP methods are used correctly
- Response formats are consistent
- Error handling follows standards
- Authentication/authorization is properly implemented
- Rate limiting is configured
- Documentation is complete
- Tests cover happy paths and edge cases
- Performance considerations are addressed
Conclusion
Following these API design guidelines will help us create consistent, maintainable, and developer-friendly APIs across all Kirana Labs projects. These guidelines should evolve over time as we learn and as technology changes.
Remember, good API design prioritizes the developer experience. A well-designed API makes integration easy and reduces the support burden.