REST API design, response formatting, versioning, pagination, filtering, sorting, error responses, status codes. Use when designing REST APIs, implementing pagination, filtering, versioning strategies, designing error response formats, or structuring API response formats.
How this skill is triggered — by the user, by Claude, or both
Slash command
/express-backend-bundle:api-design-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Master API design, response formatting, versioning, pagination, filtering, and error handling strategies.
Master API design, response formatting, versioning, pagination, filtering, and error handling strategies.
Success Response:
// 200 OK - GET, PATCH, PUT, DELETE
{
"status": "success",
"data": { /* resource */ },
"metadata": {
"timestamp": "2025-01-16T12:00:00Z",
"requestId": "uuid"
}
}
// 201 Created - POST
{
"status": "success",
"data": { /* new resource */ },
"message": "User created successfully"
}
// 204 No Content - DELETE
// (no body)
Error Response:
{
"status": "error",
"error": "Email already exists",
"code": "CONFLICT",
"timestamp": "2025-01-16T12:00:00Z",
"requestId": "uuid",
// For validation errors:
"fields": {
"email": "Email already registered",
"password": "Password must be 8+ characters"
}
}
Paginated List Response:
{
"status": "success",
"data": [ /* items */ ],
"pagination": {
"page": 1,
"limit": 20,
"total": 150,
"pages": 8,
"hasMore": true
}
}
// utils/response.ts
import { Response } from 'express';
export class ApiResponse {
static success<T>(
res: Response,
data: T,
statusCode: number = 200,
message?: string
) {
return res.status(statusCode).json({
status: 'success',
data,
...(message && { message }),
metadata: {
timestamp: new Date().toISOString()
}
});
}
static created<T>(res: Response, data: T, message?: string) {
return this.success(res, data, 201, message || 'Created successfully');
}
static noContent(res: Response) {
return res.status(204).send();
}
static error(
res: Response,
error: string,
statusCode: number = 500,
code?: string,
fields?: Record<string, string>
) {
return res.status(statusCode).json({
status: 'error',
error,
...(code && { code }),
...(fields && { fields }),
metadata: {
timestamp: new Date().toISOString()
}
});
}
static paginated<T>(
res: Response,
data: T[],
pagination: {
page: number;
limit: number;
total: number;
}
) {
const pages = Math.ceil(pagination.total / pagination.limit);
return res.json({
status: 'success',
data,
pagination: {
page: pagination.page,
limit: pagination.limit,
total: pagination.total,
pages,
hasMore: pagination.page < pages
}
});
}
}
// Usage:
// ApiResponse.success(res, user);
// ApiResponse.created(res, user, 'User created');
// ApiResponse.paginated(res, users, { page: 1, limit: 20, total: 150 });
// ApiResponse.error(res, 'Not found', 404);
// middleware/pagination.ts
import { Request, Response, NextFunction } from 'express';
export interface PaginationQuery {
page: number;
limit: number;
offset: number;
}
declare global {
namespace Express {
interface Request {
pagination?: PaginationQuery;
}
}
}
export const paginate = (maxLimit = 100) => {
return (req: Request, res: Response, next: NextFunction) => {
const page = Math.max(1, parseInt(req.query.page as string) || 1);
const limit = Math.min(
maxLimit,
parseInt(req.query.limit as string) || 20
);
req.pagination = {
page,
limit,
offset: (page - 1) * limit
};
next();
};
};
// Usage in route:
app.get('/users', paginate(100), async (req, res) => {
const { offset, limit } = req.pagination!;
const users = await db.query(
'SELECT * FROM users LIMIT $1 OFFSET $2',
[limit, offset]
);
const { rows: countResult } = await db.query('SELECT COUNT(*) FROM users');
const total = parseInt(countResult[0].count);
ApiResponse.paginated(res, users, { page, limit, total });
});
// Query string: ?status=active&role=admin&age_gt=18
interface FilterOptions {
field: string;
operator: 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'ne' | 'in';
value: any;
}
function parseFilters(query: any): FilterOptions[] {
const filters: FilterOptions[] = [];
for (const [key, value] of Object.entries(query)) {
const match = key.match(/^(\w+)(?:_(.+))?$/);
if (!match) continue;
const [, field, operator = 'eq'] = match;
filters.push({
field,
operator: operator as FilterOptions['operator'],
value
});
}
return filters;
}
// Build WHERE clause
function buildWhereClause(filters: FilterOptions[]): {
clause: string;
params: any[];
} {
const clauses: string[] = [];
const params: any[] = [];
filters.forEach(({ field, operator, value }, index) => {
const paramIndex = index + 1;
switch (operator) {
case 'eq':
clauses.push(`${field} = $${paramIndex}`);
params.push(value);
break;
case 'gt':
clauses.push(`${field} > $${paramIndex}`);
params.push(value);
break;
case 'gte':
clauses.push(`${field} >= $${paramIndex}`);
params.push(value);
break;
case 'lt':
clauses.push(`${field} < $${paramIndex}`);
params.push(value);
break;
case 'lte':
clauses.push(`${field} <= $${paramIndex}`);
params.push(value);
break;
case 'ne':
clauses.push(`${field} != $${paramIndex}`);
params.push(value);
break;
case 'in':
const ids = (value as string).split(',');
const placeholders = ids.map((_, i) => `$${paramIndex + i}`).join(',');
clauses.push(`${field} IN (${placeholders})`);
params.push(...ids);
break;
}
});
return {
clause: clauses.length ? 'WHERE ' + clauses.join(' AND ') : '',
params
};
}
// Usage: GET /users?status=active&age_gte=18&role_in=admin,moderator
// Query string: ?sort=name&sort=-created_at (- for descending)
interface SortOption {
field: string;
direction: 'ASC' | 'DESC';
}
function parseSortQuery(sort: string | string[] | undefined): SortOption[] {
if (!sort) return [];
const sortArray = Array.isArray(sort) ? sort : [sort];
const allowedFields = ['name', 'email', 'created_at', 'updated_at'];
return sortArray
.map(s => {
const isDesc = s.startsWith('-');
const field = isDesc ? s.slice(1) : s;
if (!allowedFields.includes(field)) {
throw new Error(`Invalid sort field: ${field}`);
}
return {
field,
direction: isDesc ? 'DESC' : 'ASC'
};
});
}
function buildOrderByClause(sorts: SortOption[]): string {
if (!sorts.length) return '';
return 'ORDER BY ' + sorts
.map(s => `${s.field} ${s.direction}`)
.join(', ');
}
// Usage: GET /users?sort=created_at&sort=-name
URL Path Versioning:
// /api/v1/users
// /api/v2/users
app.get('/api/v1/users', handleV1);
app.get('/api/v2/users', handleV2);
Header Versioning:
// Header: API-Version: 2.0
app.get('/api/users', (req, res) => {
const version = parseInt(req.get('API-Version') || '1');
if (version === 2) {
return handleV2(req, res);
}
handleV1(req, res);
});
Query Parameter Versioning:
// /api/users?api_version=2
app.get('/api/users', (req, res) => {
const version = parseInt(req.query.api_version as string || '1');
// ...
});
Recommended: URL Path Versioning
| Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input validation |
| 401 | Unauthorized | Missing/invalid authentication |
| 403 | Forbidden | Authenticated but no access |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource, version conflict |
| 422 | Unprocessable Entity | Validation failed |
| 429 | Too Many Requests | Rate limited |
| 500 | Server Error | Unexpected error |
| 503 | Service Unavailable | Temporarily down |
export enum ErrorCode {
VALIDATION_ERROR = 'VALIDATION_ERROR',
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
NOT_FOUND = 'NOT_FOUND',
CONFLICT = 'CONFLICT',
RATE_LIMITED = 'RATE_LIMITED',
SERVER_ERROR = 'SERVER_ERROR'
}
// Usage:
res.status(400).json({
status: 'error',
code: ErrorCode.VALIDATION_ERROR,
error: 'Email already exists',
fields: { email: 'Email must be unique' }
});
interface User {
id: string;
name: string;
email: string;
_links?: {
self: { href: string };
update: { href: string };
delete: { href: string };
};
}
function addLinks(user: User): User {
return {
...user,
_links: {
self: { href: `/api/v1/users/${user.id}` },
update: { href: `/api/v1/users/${user.id}` },
delete: { href: `/api/v1/users/${user.id}` }
}
};
}
app.get('/users/:id', (req, res) => {
const accept = req.get('Accept');
const user = { id: '1', name: 'John' };
if (accept?.includes('application/xml')) {
res.type('application/xml').send(`<user>${user.name}</user>`);
} else {
res.json(user); // Default to JSON
}
});
npx claudepluginhub karchtho/my-claude-marketplace --plugin express-backend-bundleGuides REST API design patterns for production-grade endpoints including resource naming, status codes, pagination, filtering, error responses, versioning, and rate limiting. Use when designing new APIs or reviewing contracts.
Establishes REST API design patterns for resource naming, HTTP methods and status codes, pagination, filtering, error responses, versioning, and rate limiting for production APIs.
Guides REST API design with standards for resource naming, versioning, and RFC 7807 error responses. Use when designing endpoints, pagination, or API structure.