Skip to main content

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

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

CRUD operations, business logic implementation, and API development patterns.

Core Capabilities

  1. RESTful APIs - Standard HTTP API patterns
  2. CRUD Operations - Create, Read, Update, Delete
  3. Business Logic - Service layer implementation
  4. Validation - Input and business rule validation
  5. 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-patterns instead)
  • Creating serverless functions (use serverless-patterns instead)
  • Implementing microservices (use microservices-patterns instead)
  • Working with NoSQL databases exclusively (use nosql-patterns instead)
  • Building real-time WebSocket APIs (use websocket-patterns instead)

Use these alternatives instead:

  • For GraphQL: graphql-api-patterns skill
  • For serverless: serverless-patterns skill
  • For microservices: microservices-patterns skill
  • For real-time: websocket-patterns skill

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Fat controllersBusiness logic in HTTP layerMove logic to service layer
No validationSQL injection, data corruptionUse Zod/Joi schemas on all inputs
Missing transactionsPartial updates, data inconsistencyWrap multi-step operations in transactions
Ignoring error typesGeneric 500 errorsUse specific error classes (NotFoundError, ValidationError)
N+1 queriesPerformance degradationUse JOIN or eager loading
Exposing password hashesSecurity breachNever return password_hash in responses
Trusting user inputInjection attacksAlways validate and sanitize
Using :latest in sortBySQL injectionWhitelist 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