backend-api-patterns
Backend and API implementation patterns for scalability, security, and maintainability. Use when building APIs, services, and backend infrastructure.
$ Instalar
git clone https://github.com/duyet/duyetbot-agent /tmp/duyetbot-agent && cp -r /tmp/duyetbot-agent/apps/duyetbot-action/.claude/skills/backend-api-patterns ~/.claude/skills/duyetbot-agent// tip: Run this command in your terminal to install the skill
SKILL.md
name: backend-api-patterns description: Backend and API implementation patterns for scalability, security, and maintainability. Use when building APIs, services, and backend infrastructure.
This skill provides backend and API implementation patterns for building robust, scalable services.
When to Invoke This Skill
Automatically activate for:
- API endpoint implementation
- Database operations and queries
- Authentication and authorization
- Caching and performance optimization
- Service architecture design
API Design Patterns
Consistent Response Structure
// Standard API response envelope
interface ApiResponse<T> {
data?: T;
error?: {
code: string;
message: string;
details?: Record<string, unknown>;
};
meta?: {
pagination?: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
timestamp?: string;
requestId?: string;
};
}
// Success response helper
function success<T>(data: T, meta?: ApiResponse<T>['meta']): ApiResponse<T> {
return { data, meta };
}
// Error response helper
function error(
code: string,
message: string,
details?: Record<string, unknown>
): ApiResponse<never> {
return { error: { code, message, details } };
}
// Paginated response helper
function paginated<T>(
data: T[],
page: number,
pageSize: number,
total: number
): ApiResponse<T[]> {
return {
data,
meta: {
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
},
};
}
Route Handler Pattern
// Generic handler wrapper with error handling
type Handler<T> = (
req: Request,
context: { params: Record<string, string> }
) => Promise<T>;
function createHandler<T>(handler: Handler<T>) {
return async (req: Request, context: { params: Record<string, string> }) => {
const requestId = crypto.randomUUID();
try {
const result = await handler(req, context);
return Response.json(success(result, { requestId }));
} catch (err) {
if (err instanceof AppError) {
return Response.json(
error(err.code, err.message),
{ status: err.statusCode }
);
}
console.error(`[${requestId}] Unexpected error:`, err);
return Response.json(
error('INTERNAL_ERROR', 'An unexpected error occurred'),
{ status: 500 }
);
}
};
}
// Usage
export const GET = createHandler(async (req, { params }) => {
const user = await userService.findById(params.id);
if (!user) throw new NotFoundError('User', params.id);
return user;
});
Service Layer Pattern
Repository Pattern
interface Repository<T, ID = string> {
findById(id: ID): Promise<T | null>;
findMany(options: FindOptions<T>): Promise<T[]>;
count(filter?: Partial<T>): Promise<number>;
create(data: CreateInput<T>): Promise<T>;
update(id: ID, data: UpdateInput<T>): Promise<T>;
delete(id: ID): Promise<void>;
}
interface FindOptions<T> {
filter?: Partial<T>;
orderBy?: keyof T;
orderDir?: 'asc' | 'desc';
limit?: number;
offset?: number;
}
type CreateInput<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>;
type UpdateInput<T> = Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>;
// Implementation
class UserRepository implements Repository<User> {
constructor(private db: Database) {}
async findById(id: string): Promise<User | null> {
return this.db.query.users.findFirst({
where: eq(users.id, id),
});
}
async findMany(options: FindOptions<User>): Promise<User[]> {
const { filter, orderBy, orderDir = 'asc', limit, offset } = options;
return this.db.query.users.findMany({
where: filter ? this.buildWhere(filter) : undefined,
orderBy: orderBy ? (orderDir === 'asc' ? asc : desc)(users[orderBy]) : undefined,
limit,
offset,
});
}
// ... other methods
}
Service with Business Logic
class UserService {
constructor(
private userRepo: Repository<User>,
private cache: Cache,
private eventBus: EventBus
) {}
async getUser(id: string): Promise<User> {
// Check cache first
const cached = await this.cache.get<User>(`user:${id}`);
if (cached) return cached;
// Fetch from database
const user = await this.userRepo.findById(id);
if (!user) throw new NotFoundError('User', id);
// Cache for future requests
await this.cache.set(`user:${id}`, user, { ttl: 3600 });
return user;
}
async createUser(input: CreateUserInput): Promise<User> {
// Validate
const existing = await this.userRepo.findMany({
filter: { email: input.email },
limit: 1,
});
if (existing.length > 0) {
throw new ValidationError('Email already exists', { email: 'Already in use' });
}
// Hash password
const hashedPassword = await hashPassword(input.password);
// Create user
const user = await this.userRepo.create({
...input,
password: hashedPassword,
});
// Emit event for side effects
await this.eventBus.emit('user.created', { userId: user.id });
return user;
}
async updateUser(id: string, input: UpdateUserInput): Promise<User> {
const user = await this.userRepo.update(id, input);
// Invalidate cache
await this.cache.delete(`user:${id}`);
return user;
}
}
Authentication Patterns
JWT with Refresh Tokens
interface TokenPair {
accessToken: string; // Short-lived: 15 minutes
refreshToken: string; // Long-lived: 7 days
}
interface TokenPayload {
sub: string; // User ID
email: string;
roles: string[];
type: 'access' | 'refresh';
}
class AuthService {
constructor(
private userRepo: Repository<User>,
private tokenRepo: Repository<RefreshToken>,
private jwtSecret: string
) {}
async login(email: string, password: string): Promise<TokenPair> {
const user = await this.userRepo.findMany({
filter: { email },
limit: 1,
});
if (!user[0] || !await verifyPassword(password, user[0].password)) {
throw new UnauthorizedError('Invalid credentials');
}
return this.generateTokenPair(user[0]);
}
async refresh(refreshToken: string): Promise<TokenPair> {
// Verify token
const payload = this.verifyToken(refreshToken);
if (payload.type !== 'refresh') {
throw new UnauthorizedError('Invalid token type');
}
// Check if token is revoked
const stored = await this.tokenRepo.findById(refreshToken);
if (!stored || stored.revoked) {
throw new UnauthorizedError('Token revoked');
}
// Get user and generate new tokens
const user = await this.userRepo.findById(payload.sub);
if (!user) throw new UnauthorizedError('User not found');
// Revoke old refresh token
await this.tokenRepo.update(refreshToken, { revoked: true });
return this.generateTokenPair(user);
}
private generateTokenPair(user: User): TokenPair {
const accessToken = jwt.sign(
{ sub: user.id, email: user.email, roles: user.roles, type: 'access' },
this.jwtSecret,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id, type: 'refresh' },
this.jwtSecret,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
private verifyToken(token: string): TokenPayload {
try {
return jwt.verify(token, this.jwtSecret) as TokenPayload;
} catch {
throw new UnauthorizedError('Invalid or expired token');
}
}
}
Middleware Pattern
type Middleware = (req: Request, next: () => Promise<Response>) => Promise<Response>;
// Auth middleware
function authMiddleware(requiredRoles?: string[]): Middleware {
return async (req, next) => {
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return Response.json(
error('UNAUTHORIZED', 'No token provided'),
{ status: 401 }
);
}
try {
const payload = verifyToken(token);
if (requiredRoles?.length && !requiredRoles.some(r => payload.roles.includes(r))) {
return Response.json(
error('FORBIDDEN', 'Insufficient permissions'),
{ status: 403 }
);
}
// Attach user to request context
(req as any).user = payload;
return next();
} catch {
return Response.json(
error('UNAUTHORIZED', 'Invalid or expired token'),
{ status: 401 }
);
}
};
}
// Rate limiting middleware
function rateLimitMiddleware(limit: number, windowMs: number): Middleware {
const requests = new Map<string, { count: number; resetAt: number }>();
return async (req, next) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
const now = Date.now();
const record = requests.get(ip);
if (!record || record.resetAt < now) {
requests.set(ip, { count: 1, resetAt: now + windowMs });
return next();
}
if (record.count >= limit) {
return Response.json(
error('RATE_LIMITED', 'Too many requests'),
{ status: 429 }
);
}
record.count++;
return next();
};
}
Database Patterns
Query Optimization
// Avoid N+1 queries with eager loading
async function getUsersWithOrders(): Promise<UserWithOrders[]> {
// BAD: N+1 queries
const users = await db.query.users.findMany();
for (const user of users) {
user.orders = await db.query.orders.findMany({
where: eq(orders.userId, user.id),
});
}
// GOOD: Single query with join
return db.query.users.findMany({
with: {
orders: true,
},
});
}
// Pagination with cursor
async function paginateUsers(cursor?: string, limit = 20): Promise<{
users: User[];
nextCursor: string | null;
}> {
const users = await db.query.users.findMany({
where: cursor ? gt(users.id, cursor) : undefined,
orderBy: asc(users.id),
limit: limit + 1, // Fetch one extra to check for next page
});
const hasMore = users.length > limit;
const data = hasMore ? users.slice(0, -1) : users;
return {
users: data,
nextCursor: hasMore ? data[data.length - 1].id : null,
};
}
Transaction Pattern
async function transferFunds(
fromId: string,
toId: string,
amount: number
): Promise<void> {
await db.transaction(async (tx) => {
// Lock rows for update
const from = await tx.query.accounts.findFirst({
where: eq(accounts.id, fromId),
for: 'update',
});
if (!from || from.balance < amount) {
throw new ValidationError('Insufficient funds', {});
}
// Debit source account
await tx.update(accounts)
.set({ balance: from.balance - amount })
.where(eq(accounts.id, fromId));
// Credit destination account
await tx.update(accounts)
.set({ balance: sql`${accounts.balance} + ${amount}` })
.where(eq(accounts.id, toId));
// Log transaction
await tx.insert(transactions).values({
fromId,
toId,
amount,
type: 'transfer',
});
});
}
Caching Patterns
Cache-Aside Pattern
class CachedUserService {
constructor(
private userRepo: Repository<User>,
private cache: Cache
) {}
async getUser(id: string): Promise<User | null> {
const cacheKey = `user:${id}`;
// Try cache first
const cached = await this.cache.get<User>(cacheKey);
if (cached) return cached;
// Fetch from database
const user = await this.userRepo.findById(id);
// Cache the result (including null to prevent cache stampede)
if (user) {
await this.cache.set(cacheKey, user, { ttl: 3600 });
} else {
await this.cache.set(cacheKey, null, { ttl: 60 }); // Short TTL for negative cache
}
return user;
}
async updateUser(id: string, data: UpdateUserInput): Promise<User> {
const user = await this.userRepo.update(id, data);
// Invalidate cache
await this.cache.delete(`user:${id}`);
return user;
}
}
Request Deduplication
class RequestDeduplicator {
private pending = new Map<string, Promise<unknown>>();
async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
// Return existing request if in flight
const existing = this.pending.get(key);
if (existing) return existing as Promise<T>;
// Start new request
const promise = fetcher().finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
}
}
// Usage
const deduplicator = new RequestDeduplicator();
async function getUser(id: string): Promise<User> {
return deduplicator.dedupe(`user:${id}`, () => userRepo.findById(id));
}
Best Practices Checklist
- Use consistent API response envelope
- Implement proper error hierarchy and handling
- Separate concerns: routes → services → repositories
- Use transactions for multi-step operations
- Implement caching with proper invalidation
- Avoid N+1 queries with eager loading
- Use cursor-based pagination for large datasets
- Implement rate limiting and request deduplication
- Validate inputs at API boundaries
- Log with structured data and request IDs
Repository

duyet
Author
duyet/duyetbot-agent/apps/duyetbot-action/.claude/skills/backend-api-patterns
4
Stars
0
Forks
Updated4d ago
Added1w ago