| name | backend-patterns |
| description | Apply modern backend patterns — auth middleware, caching strategies, background queues, rate limiting, and serverless/edge function design — across stacks (examples use Next.js, Node, and Supabase; adapts to your detected ecosystem). Use when the user says "backend architecture", "queue jobs", "caching layer", "rate limiting", "server actions", "edge function", "microservices", "authentication pattern", "circuit breaker", "outbox pattern", "saga", "bulkhead", "hexagonal architecture", "API gateway", or "BFF" (see references/architecture-patterns.md for the distributed-systems patterns). Pairs with design-api, audit-security, backend-realtime, and audit-backend-architecture (the read-only gap report). Do NOT use for database schema design (audit-db-schema) or pure frontend work.
|
| license | MIT |
Backend Patterns Skill
Design scalable, maintainable backend architectures using modern patterns and best practices.
Code examples lean on Next.js App Router + Supabase/Prisma. The patterns are
stack-agnostic — adapt ORMs, client libraries, and deploy targets to your detected
ecosystem.
CRITICAL: Check Existing First
Before implementing ANY backend pattern, verify:
- Check existing architecture:
ls -la src/server/ src/api/ app/api/ supabase/functions/ 2>/dev/null
cat package.json | grep -i "prisma\|drizzle\|supabase\|trpc"
- Check existing patterns:
rg "createTRPCRouter|publicProcedure" --type ts -l
rg "'use server'" --type ts -l
ls -la supabase/migrations/*.sql 2>/dev/null | tail -5
- Check database setup:
cat prisma/schema.prisma 2>/dev/null | head -50
cat supabase/config.toml 2>/dev/null
Why: Backend changes have wide impact. Understand existing architecture first.
Server Actions (Next.js 15+)
Basic Pattern
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
})
type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: string; fieldErrors?: Record<string, string[]> }
export async function createUser(
prevState: ActionResult<User> | null,
formData: FormData
): Promise<ActionResult<User>> {
const session = await auth()
if (!session?.user) {
return { success: false, error: 'Unauthorized' }
}
const result = CreateUserSchema.safeParse({
email: formData.get('email'),
name: formData.get('name'),
})
if (!result.success) {
return {
success: false,
error: 'Invalid input',
fieldErrors: result.error.flatten().fieldErrors,
}
}
try {
const user = await db.user.create({
data: result.data,
})
revalidatePath('/users')
return { success: true, data: user }
} catch (error) {
if (isPrismaError(error, 'P2002')) {
return { success: false, error: 'Email already exists' }
}
console.error('createUser error:', error)
return { success: false, error: 'Failed to create user' }
}
}
With Background Tasks
'use server'
import { after } from 'next/server'
export async function createOrder(formData: FormData) {
const order = await db.order.create({ data: { ... } })
after(async () => {
await sendOrderConfirmation(order.id)
await updateInventory(order.items)
await notifyWarehouse(order.id)
})
revalidatePath('/orders')
return { success: true, data: order }
}
tRPC Setup
Router Definition
import { z } from 'zod'
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'
export const usersRouter = createTRPCRouter({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.db.user.findUnique({
where: { id: input.id },
})
}),
create: protectedProcedure
.input(z.object({
email: z.string().email(),
name: z.string().min(1),
}))
.mutation(async ({ ctx, input }) => {
return ctx.db.user.create({
data: {
...input,
createdById: ctx.session.user.id,
},
})
}),
list: protectedProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}))
.query(async ({ ctx, input }) => {
const items = await ctx.db.user.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
})
let nextCursor: string | undefined
if (items.length > input.limit) {
const nextItem = items.pop()
nextCursor = nextItem?.id
}
return { items, nextCursor }
}),
})
Supabase Edge Functions
Basic Function
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const signature = req.headers.get('x-webhook-signature')
if (!verifySignature(signature, await req.text())) {
return new Response('Invalid signature', { status: 401 })
}
const payload = await req.json()
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
await supabase.from('events').insert({
type: payload.type,
data: payload.data,
})
return new Response(
JSON.stringify({ success: true }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
console.error('Webhook error:', error)
return new Response(
JSON.stringify({ error: 'Internal error' }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
Database Patterns
Optimistic Locking
ALTER TABLE orders ADD COLUMN version INT DEFAULT 1;
UPDATE orders
SET
status = 'shipped',
version = version + 1
WHERE id = $1 AND version = $2;
Soft Deletes
model Post {
id String @id @default(cuid())
title String
deletedAt DateTime?
@@index([deletedAt])
}
// Query active records
const posts = await db.post.findMany({
where: { deletedAt: null }
})
// Soft delete
await db.post.update({
where: { id },
data: { deletedAt: new Date() }
})
Audit Logging
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
action TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE OR REPLACE FUNCTION audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (table_name, record_id, action, old_data, new_data, user_id)
VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN row_to_json(OLD) END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) END,
auth.uid()
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit_trigger();
Caching Patterns
Next.js Cache
const data = await fetch('https://api.example.com/data', {
next: {
revalidate: 3600,
tags: ['data']
}
})
import { revalidateTag } from 'next/cache'
revalidateTag('data')
import { unstable_cache } from 'next/cache'
const getCachedUser = unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
['user'],
{ revalidate: 3600, tags: ['users'] }
)
Redis Caching
import { Redis } from '@upstash/redis'
const redis = Redis.fromEnv()
async function getCachedData<T>(
key: string,
fetcher: () => Promise<T>,
ttl = 3600
): Promise<T> {
const cached = await redis.get<T>(key)
if (cached) return cached
const data = await fetcher()
await redis.set(key, data, { ex: ttl })
return data
}
const user = await getCachedData(
`user:${id}`,
() => db.user.findUnique({ where: { id } }),
600
)
Background Jobs
Inngest
import { inngest } from './client'
export const processOrder = inngest.createFunction(
{ id: 'process-order' },
{ event: 'order/created' },
async ({ event, step }) => {
const inventory = await step.run('check-inventory', async () => {
return await checkInventory(event.data.items)
})
if (!inventory.available) {
await step.run('notify-out-of-stock', async () => {
await notifyCustomer(event.data.userId, 'out-of-stock')
})
return { status: 'cancelled' }
}
const payment = await step.run('charge-payment', async () => {
return await chargeCustomer(event.data.paymentMethod)
})
await step.run('send-confirmation', async () => {
await sendOrderConfirmation(event.data.orderId)
})
return { status: 'completed', paymentId: payment.id }
}
)
await inngest.send({
name: 'order/created',
data: { orderId, userId, items, paymentMethod }
})
Trigger.dev
import { client } from './client'
export const syncJob = client.defineJob({
id: 'sync-data',
name: 'Sync External Data',
version: '1.0.0',
trigger: intervalTrigger({ seconds: 3600 }),
run: async (payload, io, ctx) => {
const data = await io.runTask('fetch-external', async () => {
return await fetchExternalAPI()
})
await io.runTask('update-database', async () => {
await db.externalData.upsert({
where: { externalId: data.id },
create: data,
update: data,
})
})
return { synced: data.length }
},
})
Rate Limiting
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
})
export async function rateLimitedAction(userId: string) {
const { success, limit, remaining, reset } = await ratelimit.limit(userId)
if (!success) {
return {
success: false,
error: 'Too many requests',
retryAfter: Math.ceil((reset - Date.now()) / 1000),
}
}
}
Architecture patterns (distributed systems)
For the structural/distributed-systems patterns — API gateway (centralized cross-cutting
concerns), BFF / API composition, bulkhead (resource-pool isolation), circuit breaker
placement, outbox + CDC (the dual-write fix), saga (compensation + saga-pivot), hexagonal /
ports-and-adapters, anti-corruption layer, and strangler-fig migration — see
references/architecture-patterns.md for implementation guidance
and code.
Implement the pattern that fits the topology tier (don't add a mesh to a monolith or CQRS where reads
and writes don't diverge). Runtime resilience tuning (per-call timeouts, retry backoff+jitter,
idempotency keys, cancellation) lives in audit-resilience; a structural gap report comes from
audit-backend-architecture.
Validation
After implementing backend patterns:
- Error handling → All errors caught, logged, safe response returned
- Auth checks → Every mutation verifies authentication
- Input validation → Zod schema on all inputs
- Rate limiting → Sensitive endpoints protected
- Idempotency → Critical operations handle retries
- Logging → Structured logs without sensitive data
- Testing → Unit tests for business logic, integration for APIs