Skip to main content

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:

MethodPurposeExample
GETRetrieve data without side effectsGET /products
POSTCreate new resourcesPOST /products
PUTReplace a resource entirelyPUT /products/123
PATCHUpdate a resource partiallyPATCH /products/123
DELETERemove a resourceDELETE /products/123

Request/Response Formats​

Content Types​

  • Default to application/json for request and response bodies
  • Support application/x-www-form-urlencoded where necessary
  • Use multipart/form-data for 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": "[email protected]",
"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 succeeded
  • 201 Created: Resource created successfully
  • 204 No Content: Request succeeded, but no content to return

Client Error Codes​

  • 400 Bad Request: Malformed request or invalid data
  • 401 Unauthorized: Authentication required
  • 403 Forbidden: Authentication succeeded, but no permission
  • 404 Not Found: Resource not found
  • 422 Unprocessable Entity: Validation errors

Server Error Codes​

  • 500 Internal Server Error: Unexpected server error
  • 503 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:

CodeDescription
VALIDATION_ERRORInput validation failed
AUTHENTICATION_ERRORAuthentication failed
AUTHORIZATION_ERRORPermission denied
RESOURCE_NOT_FOUNDRequested resource not found
RESOURCE_CONFLICTResource already exists
RATE_LIMIT_EXCEEDEDToo 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 Unauthorized for invalid or missing tokens

Authorization​

  • Check permissions after authentication
  • Return 403 Forbidden for 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 Requests when limit exceeded

Caching​

Support caching where appropriate:

  • Use ETag and If-None-Match headers
  • Return 304 Not Modified when content unchanged
  • Set appropriate Cache-Control headers:
    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: '[email protected]' },
{ id: 2, name: 'Jane', email: '[email protected]' },
];

(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: '[email protected]',
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.