backend-patterns
SKILL.md
Backend Patterns Skill
Modern backend patterns and best practices for building scalable APIs.
API Design Patterns
RESTful Conventions
GET /api/v1/users # List users
GET /api/v1/users/:id # Get user
POST /api/v1/users # Create user
PUT /api/v1/users/:id # Replace user
PATCH /api/v1/users/:id # Update user
DELETE /api/v1/users/:id # Delete user
# Nested resources
GET /api/v1/users/:id/posts
POST /api/v1/users/:id/posts
# Actions as resources
POST /api/v1/auth/login
POST /api/v1/auth/logout
POST /api/v1/passwords/reset
Response Format
// Success response
interface SuccessResponse<T> {
success: true;
data: T;
meta?: {
page: number;
perPage: number;
total: number;
totalPages: number;
};
}
// Error response
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: Array<{
field: string;
message: string;
}>;
};
}
Status Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Validation error |
| 401 | Unauthorized | Missing/invalid auth |
| 403 | Forbidden | No permission |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate resource |
| 422 | Unprocessable | Business logic error |
| 500 | Server Error | Unexpected error |
Authentication Patterns
JWT Flow
// Login - generate tokens
async function login(email: string, password: string) {
const user = await db.users.findByEmail(email);
if (!user || !await verifyPassword(password, user.password)) {
throw new HTTPException(401, { message: 'Invalid credentials' });
}
const accessToken = await generateAccessToken(user);
const refreshToken = await generateRefreshToken(user);
// Store refresh token
await db.refreshTokens.create({
userId: user.id,
token: refreshToken,
expiresAt: addDays(new Date(), 7),
});
return { accessToken, refreshToken, user };
}
// Token generation
async function generateAccessToken(user: User) {
return jwt.sign(
{ sub: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
}
// Token refresh
async function refreshAccessToken(refreshToken: string) {
const stored = await db.refreshTokens.findByToken(refreshToken);
if (!stored || stored.expiresAt < new Date()) {
throw new HTTPException(401, { message: 'Invalid refresh token' });
}
const user = await db.users.findById(stored.userId);
return generateAccessToken(user);
}
Auth Middleware
export async function authMiddleware(c: Context, next: Next) {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new HTTPException(401, { message: 'Missing token' });
}
const token = authHeader.slice(7);
try {
const payload = await jwt.verify(token, process.env.JWT_SECRET);
c.set('userId', payload.sub);
c.set('userRole', payload.role);
await next();
} catch {
throw new HTTPException(401, { message: 'Invalid token' });
}
}
// Role-based authorization
export function requireRole(...roles: string[]) {
return async (c: Context, next: Next) => {
const userRole = c.get('userRole');
if (!roles.includes(userRole)) {
throw new HTTPException(403, { message: 'Forbidden' });
}
await next();
};
}
Validation Patterns
Zod Schemas
import { z } from 'zod';
// User schemas
export const createUserSchema = z.object({
email: z.string().email('Invalid email'),
name: z.string().min(2).max(100),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[0-9]/, 'Must contain number'),
});
export const updateUserSchema = createUserSchema.partial();
// Query params
export const paginationSchema = z.object({
page: z.coerce.number().min(1).default(1),
perPage: z.coerce.number().min(1).max(100).default(20),
sort: z.enum(['createdAt', 'name']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc'),
});
// Using with Hono
app.post('/users', zValidator('json', createUserSchema), async (c) => {
const data = c.req.valid('json');
// data is fully typed
});
Error Handling
Central Error Handler
export function errorHandler(err: Error, c: Context) {
console.error('Error:', err);
// HTTP exceptions
if (err instanceof HTTPException) {
return c.json({
success: false,
error: { code: 'HTTP_ERROR', message: err.message },
}, err.status);
}
// Validation errors
if (err instanceof ZodError) {
return c.json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: err.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
},
}, 400);
}
// Database errors
if (err.code === '23505') { // Unique violation
return c.json({
success: false,
error: { code: 'CONFLICT', message: 'Resource already exists' },
}, 409);
}
// Unknown errors
return c.json({
success: false,
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
}, 500);
}
Caching Patterns
Redis Caching
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Cache-aside pattern
async function getUserById(id: string): Promise<User> {
const cacheKey = `user:${id}`;
// Try cache first
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await db.users.findById(id);
if (!user) throw new HTTPException(404);
// Cache for 5 minutes
await redis.setex(cacheKey, 300, JSON.stringify(user));
return user;
}
// Invalidate on update
async function updateUser(id: string, data: UpdateUserInput) {
const user = await db.users.update(id, data);
await redis.del(`user:${id}`);
return user;
}
Rate Limiting
import { rateLimiter } from 'hono-rate-limiter';
// Apply rate limiting
app.use(
'/api/*',
rateLimiter({
windowMs: 60 * 1000, // 1 minute
limit: 100, // 100 requests per minute
keyGenerator: (c) => c.get('userId') || c.req.header('x-forwarded-for'),
})
);
// Stricter limits for auth endpoints
app.use(
'/api/auth/*',
rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 5, // 5 attempts
})
);
Database Patterns
Repository Pattern
class UserRepository {
async findById(id: string): Promise<User | null> {
return db.query.users.findFirst({
where: eq(users.id, id),
});
}
async findByEmail(email: string): Promise<User | null> {
return db.query.users.findFirst({
where: eq(users.email, email),
});
}
async create(data: CreateUserInput): Promise<User> {
const [user] = await db.insert(users).values(data).returning();
return user;
}
async update(id: string, data: UpdateUserInput): Promise<User> {
const [user] = await db
.update(users)
.set({ ...data, updatedAt: new Date() })
.where(eq(users.id, id))
.returning();
return user;
}
async delete(id: string): Promise<void> {
await db.delete(users).where(eq(users.id, id));
}
}
Transaction Pattern
async function createOrder(userId: string, items: OrderItem[]) {
return db.transaction(async (tx) => {
// Create order
const [order] = await tx
.insert(orders)
.values({ userId, status: 'pending' })
.returning();
// Create order items
await tx.insert(orderItems).values(
items.map(item => ({
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
}))
);
// Update inventory
for (const item of items) {
await tx
.update(products)
.set({ stock: sql`stock - ${item.quantity}` })
.where(eq(products.id, item.productId));
}
return order;
});
}
Weekly Installs
3
Repository
claudeforge/orchestratorGitHub Stars
36
First Seen
13 days ago
Security Audits
Installed on
opencode3
github-copilot3
codex3
amp3
cline3
kimi-cli3