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": "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 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: '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.