Agent Skills Framework Extension
Backend Development Patterns Skill
When to Use This Skill
Use this skill when implementing backend development patterns patterns in your codebase.
How to Use This Skill
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- Follow the best practices outlined in this skill
CRUD operations, business logic implementation, and API development patterns.
Core Capabilities
- RESTful APIs - Standard HTTP API patterns
- CRUD Operations - Create, Read, Update, Delete
- Business Logic - Service layer implementation
- Validation - Input and business rule validation
- Transactions - Database transaction patterns
RESTful API Structure
// src/api/routes/users.ts
import { Router } from 'express';
import { UserController } from '../controllers/user-controller';
import { validate } from '../middleware/validation';
import { authenticate, authorize } from '../middleware/auth';
import { userSchemas } from '../schemas/user-schemas';
const router = Router();
const controller = new UserController();
// Collection routes
router.get('/',
authenticate,
authorize('users:read'),
validate(userSchemas.list, 'query'),
controller.list
);
router.post('/',
authenticate,
authorize('users:create'),
validate(userSchemas.create, 'body'),
controller.create
);
// Resource routes
router.get('/:id',
authenticate,
authorize('users:read'),
validate(userSchemas.getById, 'params'),
controller.getById
);
router.put('/:id',
authenticate,
authorize('users:update'),
validate(userSchemas.update, 'body'),
controller.update
);
router.patch('/:id',
authenticate,
authorize('users:update'),
validate(userSchemas.patch, 'body'),
controller.patch
);
router.delete('/:id',
authenticate,
authorize('users:delete'),
validate(userSchemas.getById, 'params'),
controller.delete
);
// Nested routes
router.get('/:id/orders',
authenticate,
authorize('orders:read'),
controller.getUserOrders
);
export default router;
Controller Pattern
// src/api/controllers/user-controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../../services/user-service';
import { CreateUserDTO, UpdateUserDTO, UserFilters } from '../../dtos/user-dto';
import { NotFoundError, ConflictError } from '../../errors';
export class UserController {
constructor(private readonly userService: UserService = new UserService()) {}
list = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const filters: UserFilters = {
search: req.query.search as string,
status: req.query.status as string,
role: req.query.role as string,
};
const pagination = {
page: parseInt(req.query.page as string) || 1,
limit: parseInt(req.query.limit as string) || 20,
sortBy: req.query.sortBy as string || 'createdAt',
sortOrder: (req.query.sortOrder as 'asc' | 'desc') || 'desc',
};
const result = await this.userService.findAll(filters, pagination);
res.json({
data: result.items,
meta: {
page: pagination.page,
limit: pagination.limit,
total: result.total,
totalPages: Math.ceil(result.total / pagination.limit),
},
});
} catch (error) {
next(error);
}
};
getById = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const user = await this.userService.findById(req.params.id);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
res.json({ data: user });
} catch (error) {
next(error);
}
};
create = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const dto: CreateUserDTO = req.body;
const user = await this.userService.create(dto);
res.status(201).json({ data: user });
} catch (error) {
next(error);
}
};
update = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const dto: UpdateUserDTO = req.body;
const user = await this.userService.update(req.params.id, dto);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
res.json({ data: user });
} catch (error) {
next(error);
}
};
patch = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const user = await this.userService.patch(req.params.id, req.body);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
res.json({ data: user });
} catch (error) {
next(error);
}
};
delete = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const deleted = await this.userService.delete(req.params.id);
if (!deleted) {
throw new NotFoundError('User', req.params.id);
}
res.status(204).send();
} catch (error) {
next(error);
}
};
getUserOrders = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const orders = await this.userService.getOrders(req.params.id);
res.json({ data: orders });
} catch (error) {
next(error);
}
};
}
Service Layer
// src/services/user-service.ts
import { Pool } from 'pg';
import { User, UserRole, UserStatus } from '../models/user';
import { CreateUserDTO, UpdateUserDTO, UserFilters, PaginatedResult } from '../dtos/user-dto';
import { PasswordService } from './password-service';
import { EmailService } from './email-service';
import { ConflictError, ValidationError } from '../errors';
interface Pagination {
page: number;
limit: number;
sortBy: string;
sortOrder: 'asc' | 'desc';
}
export class UserService {
constructor(
private readonly db: Pool,
private readonly passwordService: PasswordService,
private readonly emailService: EmailService
) {}
async findAll(filters: UserFilters, pagination: Pagination): Promise<PaginatedResult<User>> {
const { page, limit, sortBy, sortOrder } = pagination;
const offset = (page - 1) * limit;
// Build WHERE clause
const conditions: string[] = [];
const params: unknown[] = [];
let paramIndex = 1;
if (filters.search) {
conditions.push(`(name ILIKE $${paramIndex} OR email ILIKE $${paramIndex})`);
params.push(`%${filters.search}%`);
paramIndex++;
}
if (filters.status) {
conditions.push(`status = $${paramIndex}`);
params.push(filters.status);
paramIndex++;
}
if (filters.role) {
conditions.push(`role = $${paramIndex}`);
params.push(filters.role);
paramIndex++;
}
const whereClause = conditions.length > 0
? `WHERE ${conditions.join(' AND ')}`
: '';
// Validate sortBy to prevent SQL injection
const allowedSortFields = ['created_at', 'updated_at', 'name', 'email'];
const safeSortBy = allowedSortFields.includes(sortBy) ? sortBy : 'created_at';
const safeSortOrder = sortOrder === 'asc' ? 'ASC' : 'DESC';
// Count query
const countResult = await this.db.query(
`SELECT COUNT(*) FROM users ${whereClause}`,
params
);
const total = parseInt(countResult.rows[0].count);
// Data query
const dataResult = await this.db.query(
`SELECT id, email, name, role, status, created_at, updated_at
FROM users
${whereClause}
ORDER BY ${safeSortBy} ${safeSortOrder}
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
[...params, limit, offset]
);
return {
items: dataResult.rows.map(this.mapToUser),
total,
};
}
async findById(id: string): Promise<User | null> {
const result = await this.db.query(
`SELECT id, email, name, role, status, created_at, updated_at
FROM users WHERE id = $1`,
[id]
);
return result.rows.length > 0 ? this.mapToUser(result.rows[0]) : null;
}
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query(
`SELECT id, email, name, role, status, password_hash, created_at, updated_at
FROM users WHERE email = $1`,
[email.toLowerCase()]
);
return result.rows.length > 0 ? this.mapToUser(result.rows[0]) : null;
}
async create(dto: CreateUserDTO): Promise<User> {
// Check for existing email
const existing = await this.findByEmail(dto.email);
if (existing) {
throw new ConflictError('User with this email already exists');
}
// Hash password
const passwordHash = await this.passwordService.hash(dto.password);
// Insert user
const result = await this.db.query(
`INSERT INTO users (email, name, password_hash, role, status)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, email, name, role, status, created_at, updated_at`,
[
dto.email.toLowerCase(),
dto.name,
passwordHash,
dto.role || UserRole.USER,
UserStatus.PENDING,
]
);
const user = this.mapToUser(result.rows[0]);
// Send welcome email
await this.emailService.sendWelcome(user);
return user;
}
async update(id: string, dto: UpdateUserDTO): Promise<User | null> {
// Check if user exists
const existing = await this.findById(id);
if (!existing) return null;
// Check email uniqueness if changing
if (dto.email && dto.email.toLowerCase() !== existing.email) {
const emailExists = await this.findByEmail(dto.email);
if (emailExists) {
throw new ConflictError('Email already in use');
}
}
const result = await this.db.query(
`UPDATE users
SET name = $2, email = $3, role = $4, updated_at = NOW()
WHERE id = $1
RETURNING id, email, name, role, status, created_at, updated_at`,
[id, dto.name, dto.email?.toLowerCase(), dto.role]
);
return this.mapToUser(result.rows[0]);
}
async patch(id: string, updates: Partial<UpdateUserDTO>): Promise<User | null> {
const existing = await this.findById(id);
if (!existing) return null;
// Build dynamic update
const fields: string[] = [];
const values: unknown[] = [];
let paramIndex = 2;
if (updates.name !== undefined) {
fields.push(`name = $${paramIndex++}`);
values.push(updates.name);
}
if (updates.email !== undefined) {
fields.push(`email = $${paramIndex++}`);
values.push(updates.email.toLowerCase());
}
if (updates.role !== undefined) {
fields.push(`role = $${paramIndex++}`);
values.push(updates.role);
}
if (fields.length === 0) {
return existing;
}
fields.push('updated_at = NOW()');
const result = await this.db.query(
`UPDATE users SET ${fields.join(', ')} WHERE id = $1
RETURNING id, email, name, role, status, created_at, updated_at`,
[id, ...values]
);
return this.mapToUser(result.rows[0]);
}
async delete(id: string): Promise<boolean> {
const result = await this.db.query(
'DELETE FROM users WHERE id = $1',
[id]
);
return result.rowCount > 0;
}
async activate(id: string): Promise<User | null> {
const result = await this.db.query(
`UPDATE users SET status = $2, updated_at = NOW()
WHERE id = $1
RETURNING id, email, name, role, status, created_at, updated_at`,
[id, UserStatus.ACTIVE]
);
return result.rows.length > 0 ? this.mapToUser(result.rows[0]) : null;
}
private mapToUser(row: any): User {
return {
id: row.id,
email: row.email,
name: row.name,
role: row.role,
status: row.status,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
}
}
Transaction Patterns
// src/services/order-service.ts
export class OrderService {
constructor(private readonly db: Pool) {}
async createOrder(dto: CreateOrderDTO): Promise<Order> {
const client = await this.db.connect();
try {
await client.query('BEGIN');
// 1. Create order
const orderResult = await client.query(
`INSERT INTO orders (customer_id, status, total)
VALUES ($1, 'pending', 0)
RETURNING id`,
[dto.customerId]
);
const orderId = orderResult.rows[0].id;
// 2. Add items and calculate total
let total = 0;
for (const item of dto.items) {
// Check stock availability
const stockResult = await client.query(
`SELECT stock, price FROM products WHERE id = $1 FOR UPDATE`,
[item.productId]
);
if (stockResult.rows.length === 0) {
throw new NotFoundError('Product', item.productId);
}
const product = stockResult.rows[0];
if (product.stock < item.quantity) {
throw new ValidationError(`Insufficient stock for product ${item.productId}`);
}
// Insert order item
await client.query(
`INSERT INTO order_items (order_id, product_id, quantity, unit_price)
VALUES ($1, $2, $3, $4)`,
[orderId, item.productId, item.quantity, product.price]
);
// Update stock
await client.query(
`UPDATE products SET stock = stock - $2 WHERE id = $1`,
[item.productId, item.quantity]
);
total += product.price * item.quantity;
}
// 3. Update order total
await client.query(
`UPDATE orders SET total = $2 WHERE id = $1`,
[orderId, total]
);
await client.query('COMMIT');
return this.findById(orderId);
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async cancelOrder(orderId: string): Promise<void> {
const client = await this.db.connect();
try {
await client.query('BEGIN');
// Get order items
const itemsResult = await client.query(
`SELECT product_id, quantity FROM order_items WHERE order_id = $1`,
[orderId]
);
// Restore stock for each item
for (const item of itemsResult.rows) {
await client.query(
`UPDATE products SET stock = stock + $2 WHERE id = $1`,
[item.product_id, item.quantity]
);
}
// Update order status
await client.query(
`UPDATE orders SET status = 'cancelled', updated_at = NOW() WHERE id = $1`,
[orderId]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
}
Validation Schemas
// src/api/schemas/user-schemas.ts
import { z } from 'zod';
export const userSchemas = {
list: z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
search: z.string().max(100).optional(),
status: z.enum(['active', 'inactive', 'pending']).optional(),
role: z.enum(['admin', 'manager', 'user']).optional(),
sortBy: z.enum(['createdAt', 'updatedAt', 'name', 'email']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc'),
}),
getById: z.object({
id: z.string().uuid(),
}),
create: z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
password: z.string()
.min(8)
.max(100)
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number'),
role: z.enum(['admin', 'manager', 'user']).optional(),
}),
update: z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'manager', 'user']),
}),
patch: z.object({
email: z.string().email().max(255).optional(),
name: z.string().min(1).max(100).optional(),
role: z.enum(['admin', 'manager', 'user']).optional(),
}).refine(data => Object.keys(data).length > 0, {
message: 'At least one field must be provided',
}),
};
Usage Examples
Create RESTful API
Apply backend-development-patterns skill to scaffold CRUD API for products resource
Implement Service Layer
Apply backend-development-patterns skill to create service layer with transaction support
Add Validation
Apply backend-development-patterns skill to add Zod validation schemas for API endpoints
Success Output
When successful, this skill MUST output:
✅ SKILL COMPLETE: backend-development-patterns
Completed:
- [x] RESTful API routes defined with proper HTTP methods
- [x] Controller layer implemented with error handling
- [x] Service layer created with business logic separation
- [x] Validation schemas defined (Zod/Joi)
- [x] Transaction patterns implemented (where applicable)
Outputs:
- routes/[resource].ts - API route definitions
- controllers/[resource]-controller.ts - Request handlers
- services/[resource]-service.ts - Business logic
- schemas/[resource]-schemas.ts - Validation rules
- Test coverage: [X]%
Completion Checklist
Before marking this skill as complete, verify:
- All CRUD operations implemented (Create, Read, Update, Delete)
- Route handlers include authentication and authorization middleware
- Input validation schemas cover all endpoints
- Service layer separates business logic from HTTP concerns
- Database transactions used for multi-step operations
- Error handling includes proper HTTP status codes
- Pagination implemented for list endpoints
- SQL injection prevention (parameterized queries)
Failure Indicators
This skill has FAILED if:
- ❌ Routes directly access database without service layer
- ❌ No input validation on endpoints
- ❌ SQL queries use string concatenation instead of parameters
- ❌ Missing error handling in controllers (no try-catch)
- ❌ Transactions not used for multi-step operations
- ❌ No authentication/authorization middleware
- ❌ List endpoints return all records without pagination
- ❌ Validation errors return 500 instead of 400
When NOT to Use
Do NOT use this skill when:
- Building GraphQL APIs (use
graphql-api-patternsinstead) - Creating serverless functions (use
serverless-patternsinstead) - Implementing microservices (use
microservices-patternsinstead) - Working with NoSQL databases exclusively (use
nosql-patternsinstead) - Building real-time WebSocket APIs (use
websocket-patternsinstead)
Use these alternatives instead:
- For GraphQL:
graphql-api-patternsskill - For serverless:
serverless-patternsskill - For microservices:
microservices-patternsskill - For real-time:
websocket-patternsskill
Anti-Patterns (Avoid)
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Fat controllers | Business logic in HTTP layer | Move logic to service layer |
| No validation | SQL injection, data corruption | Use Zod/Joi schemas on all inputs |
| Missing transactions | Partial updates, data inconsistency | Wrap multi-step operations in transactions |
| Ignoring error types | Generic 500 errors | Use specific error classes (NotFoundError, ValidationError) |
| N+1 queries | Performance degradation | Use JOIN or eager loading |
| Exposing password hashes | Security breach | Never return password_hash in responses |
| Trusting user input | Injection attacks | Always validate and sanitize |
| Using :latest in sortBy | SQL injection | Whitelist allowed sort fields |
Principles
This skill embodies:
- #3 Keep It Simple - Three-layer architecture (routes → controllers → services)
- #4 Separation of Concerns - HTTP, business logic, and data access separated
- #5 Eliminate Ambiguity - Clear error types and HTTP status codes
- #8 No Assumptions - Validate all inputs, check existence before operations
- #2 First Principles - RESTful conventions, not custom protocols
Layered Architecture: This skill enforces clean separation between HTTP concerns (controllers), business logic (services), and data access (repositories/SQL).
Integration Points
- backend-architecture-patterns - Clean architecture structure
- database-schema-optimization - Query optimization
- error-handling-resilience - Error handling patterns