| name | senior-fullstack |
| description | Expert fullstack architecture skill covering advanced backend patterns, infrastructure, testing, and system design. Use this skill whenever the user is working on fullstack architecture decisions, API design, background job queues, caching strategies, database connection pooling, CI/CD pipelines, Docker containerization, end-to-end testing with Playwright, WebSocket real-time features, or deployment configurations. Trigger on mentions of Redis, BullMQ, Prisma, Drizzle, PostgreSQL, Docker, GitHub Actions, connection pooling, rate limiting, Playwright, or any backend/infra topic that goes beyond simple CRUD. Also trigger for performance bottlenecks, scalability concerns, or production readiness reviews.
|
Senior Fullstack Engineer Skill
Advanced architectural patterns, infrastructure, testing, and production-grade system design.
Core Stack
| Layer | Technology |
|---|
| Framework | Next.js 15 (App Router) |
| ORM | Drizzle ORM or Prisma |
| Database | PostgreSQL (Supabase / Neon / RDS) |
| Cache / Queue | Redis + BullMQ |
| Auth | Clerk or NextAuth v5 |
| Payments | Stripe |
| Testing | Playwright (E2E) + Vitest (unit) |
| CI/CD | GitHub Actions |
| Containers | Docker + Docker Compose |
| Monitoring | Sentry + OpenTelemetry |
Database Layer
Drizzle ORM (Preferred — Type-Safe, Lightweight)
import { pgTable, text, timestamp, uuid, boolean, index } from 'drizzle-orm/pg-core'
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
clerkId: text('clerk_id').notNull().unique(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
}, (table) => ({
clerkIdIdx: index('users_clerk_id_idx').on(table.clerkId),
emailIdx: index('users_email_idx').on(table.email),
}))
export const posts = pgTable('posts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content'),
published: boolean('published').notNull().default(false),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (table) => ({
userIdIdx: index('posts_user_id_idx').on(table.userId),
slugIdx: index('posts_slug_idx').on(table.slug),
}))
Database Connection Pooling
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 2_000,
})
export const db = drizzle(pool, { schema })
Repository Pattern
import { db } from '@/lib/db'
import { posts, users } from '@/lib/db/schema'
import { eq, desc, and, sql } from 'drizzle-orm'
export class PostRepository {
async findPublished(limit = 10, offset = 0) {
return db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
publishedAt: posts.publishedAt,
author: { name: users.name },
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.publishedAt))
.limit(limit)
.offset(offset)
}
async findBySlug(slug: string) {
const [post] = await db
.select()
.from(posts)
.where(eq(posts.slug, slug))
.limit(1)
return post ?? null
}
async create(data: typeof posts.$inferInsert) {
const [post] = await db.insert(posts).values(data).returning()
return post
}
async incrementView(id: string) {
await db
.update(posts)
.set({ viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, id))
}
}
export const postRepository = new PostRepository()
Redis Caching
Setup & Client
import { Redis } from 'ioredis'
let redis: Redis
if (process.env.NODE_ENV === 'production') {
redis = new Redis(process.env.REDIS_URL!, {
maxRetriesPerRequest: 3,
enableReadyCheck: false,
lazyConnect: true,
})
} else {
const globalForRedis = globalThis as { redis?: Redis }
globalForRedis.redis ??= new Redis(process.env.REDIS_URL!)
redis = globalForRedis.redis
}
export { redis }
Cache-Aside Pattern
import { redis } from './redis'
export async function cached<T>(
key: string,
fn: () => Promise<T>,
ttlSeconds = 60
): Promise<T> {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached) as T
const result = await fn()
await redis.setex(key, ttlSeconds, JSON.stringify(result))
return result
}
export function cacheKey(...parts: string[]) {
return parts.join(':')
}
const posts = await cached(
cacheKey('posts', 'published', page.toString()),
() => postRepository.findPublished(10, offset),
300
)
Cache Invalidation Strategy
export async function invalidatePostCache(postId: string, slug: string) {
const keys = await redis.keys(`posts:*`)
if (keys.length > 0) await redis.del(...keys)
await redis.del(`post:${slug}`, `post:${postId}`)
}
Background Jobs with BullMQ
Queue Setup
import { Queue, Worker, QueueEvents } from 'bullmq'
import { redis } from '@/lib/redis'
const connection = { connection: redis }
export const emailQueue = new Queue('email', connection)
export const imageQueue = new Queue('image-processing', connection)
export const webhookQueue = new Queue('webhooks', connection)
export type EmailJobData = {
to: string
subject: string
template: 'welcome' | 'reset-password' | 'invoice'
variables: Record<string, string>
}
Worker
import { Worker } from 'bullmq'
import { redis } from '@/lib/redis'
import { sendEmail } from '@/lib/email'
import type { EmailJobData } from '@/lib/queues'
export const emailWorker = new Worker<EmailJobData>(
'email',
async (job) => {
const { to, subject, template, variables } = job.data
await job.updateProgress(10)
const html = await renderEmailTemplate(template, variables)
await job.updateProgress(50)
await sendEmail({ to, subject, html })
await job.updateProgress(100)
return { sent: true, to }
},
{
connection: { connection: redis },
concurrency: 5,
limiter: { max: 100, duration: 60_000 },
}
)
emailWorker.on('completed', (job, result) => {
console.log(`Email job ${job.id} completed`, result)
})
emailWorker.on('failed', (job, err) => {
console.error(`Email job ${job?.id} failed:`, err)
})
Enqueue from Server Actions
'use server'
import { emailQueue } from '@/lib/queues'
export async function sendWelcomeEmail(userId: string) {
const user = await userRepository.findById(userId)
if (!user) return
await emailQueue.add(
'welcome-email',
{
to: user.email,
subject: 'Welcome!',
template: 'welcome',
variables: { name: user.name },
},
{
attempts: 3,
backoff: { type: 'exponential', delay: 5000 },
removeOnComplete: 100,
removeOnFail: 500,
}
)
}
API Design (Route Handlers)
Typed Route Handler
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@clerk/nextjs/server'
import { z } from 'zod'
import { postRepository } from '@/lib/db/repositories/post.repository'
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().optional(),
published: z.boolean().default(false),
})
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const page = Math.max(1, Number(searchParams.get('page') ?? 1))
const limit = Math.min(50, Number(searchParams.get('limit') ?? 10))
const posts = await postRepository.findPublished(limit, (page - 1) * limit)
return NextResponse.json({ data: posts, page, limit })
}
export async function POST(request: NextRequest) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const parsed = createPostSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', details: parsed.error.flatten() },
{ status: 422 }
)
}
const post = await postRepository.create({ ...parsed.data, userId })
return NextResponse.json({ data: post }, { status: 201 })
}
Rate Limiting Middleware
import { NextResponse, type NextRequest } from 'next/server'
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 middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? '127.0.0.1'
const { success, limit, reset, remaining } = await ratelimit.limit(ip)
if (!success) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
}
)
}
}
}
export const config = {
matcher: '/api/:path*',
}
Testing
Playwright E2E
import { test, expect } from '@playwright/test'
test.describe('Authentication flow', () => {
test('user can sign up and access dashboard', async ({ page }) => {
await page.goto('/sign-up')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('protected routes redirect unauthenticated users', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL(/\/sign-in/)
})
})
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html'], ['github']],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
Vitest Unit Tests
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { cached } from '@/lib/cache'
vi.mock('@/lib/redis', () => ({
redis: {
get: vi.fn(),
setex: vi.fn(),
},
}))
describe('cached()', () => {
beforeEach(() => vi.clearAllMocks())
it('returns cached value when cache hit', async () => {
const { redis } = await import('@/lib/redis')
vi.mocked(redis.get).mockResolvedValue(JSON.stringify({ id: 1 }))
const fn = vi.fn().mockResolvedValue({ id: 1 })
const result = await cached('test-key', fn)
expect(result).toEqual({ id: 1 })
expect(fn).not.toHaveBeenCalled()
})
it('calls fn and caches result on miss', async () => {
const { redis } = await import('@/lib/redis')
vi.mocked(redis.get).mockResolvedValue(null)
const fn = vi.fn().mockResolvedValue({ id: 2 })
const result = await cached('test-key', fn, 60)
expect(result).toEqual({ id: 2 })
expect(redis.setex).toHaveBeenCalledWith('test-key', 60, JSON.stringify({ id: 2 }))
})
})
Docker & CI/CD
Multi-Stage Dockerfile
# Dockerfile
FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app
RUN npm install -g pnpm
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build
FROM base AS runner
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
GitHub Actions CI Pipeline
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Type check
run: pnpm tsc --noEmit
- name: Lint
run: pnpm lint
- name: Unit tests
run: pnpm test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: Run migrations
run: pnpm db:migrate
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: Build
run: pnpm build
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
e2e:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm playwright test
env:
BASE_URL: http://localhost:3000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Key Architectural Rules
- Repository pattern for all DB access — never raw queries in route handlers or Server Actions
- Connection pool as singleton — one
Pool instance, not one per request
- BullMQ for anything async — email, image processing, webhooks, notifications
- Redis cache-aside — cache at the repository layer, not the component layer
- Zod validation at every API boundary — route handlers AND Server Actions
- Rate limit all public APIs — use Upstash Redis for serverless-safe limiting
- Multi-stage Docker builds — never ship dev dependencies to production
- Test pyramid: unit (Vitest) > integration > E2E (Playwright)
- CI gates on type check + lint + unit tests before E2E
- Never commit secrets — use environment variables, validate at startup