AI Agents,  Code,  Console

Claude Code Crash Course

A comprehensive, production-grade tutorial for mastering Claude Code in backend development with TypeScript + Express + GraphQL. Based on real-world best practices.


Table of Contents

  1. Introduction
  2. Core Philosophy: Input Quality Determines Output Quality
  3. Installation and Setup
  4. Plan Mode and the AskUserQuestion Tool
  5. Build Without Ralph: Gaining Experience
  6. Understanding Ralph Loops
  7. SPECS.md and Progress.txt: Your Project Documents
  8. Skills, MCP and CLAUDE.md
  9. Context Management
  10. CLI Reference and Essential Commands
  11. Tips and Tricks: Expert-Level Practices
  12. Real-World Example: Express + GraphQL Backend
  13. Resources and Links

Models are good enough now

If your output is garbage, your input was garbage. Invest in planning.


Introduction

Claude Code is Anthropic’s agentic coding tool that lives in your terminal. Unlike traditional chatbots, Claude Code can read files, execute commands, make changes, and work autonomously on problems. This tutorial focuses on maximizing your effectiveness as a backend developer building production systems.

Target Audience: Senior backend developers familiar with:

  • TypeScript/Node.js ecosystems
  • Express.js and GraphQL (Apollo Server)
  • Testing frameworks (Jest, Supertest, Testcontainers)
  • CI/CD pipelines and Docker
  • PostgreSQL with native pg client
  • Dependency injection pattern (DI Container)
  • Observability (OpenTelemetry, Prometheus)

Key Insight: Models have become extraordinarily capable. If you’re producing «AI garbage,» it’s because your inputs were garbage. The era of blaming the model is over—invest in quality inputs.


Core Philosophy

The Software Engineering Mindset

Think of communicating with Claude Code like communicating with a human engineer:

❌ Sparse Instructions✅ Precise Instructions
«Build me an auth system»«Build a JWT-based auth system with refresh tokens, password hashing with argon2, rate limiting (100 req/15min), and PostgreSQL user storage with UserRepository receiving db pool via constructor injection, registered in the DI container»
«Add an API endpoint»«Add a POST /api/v1/users endpoint that validates input with Zod, creates a user via UserService (resolved from DI container), returns 201 with the user DTO, and handles duplicate email errors with 409»
«Fix the bug»«Login fails after session timeout. Check the token refresh logic in src/auth/. Write a failing test first, then fix it»

Features, Not Products

When planning, think in features, not products. A product is abstract; features are concrete and testable.

Product: "A user management API"
    │
    ├── Feature 1: User registration with email verification
    │   └── Test: Email sent, user status = pending
    │
    ├── Feature 2: JWT authentication with refresh tokens
    │   └── Test: Token issued, refresh works, expired = 401
    │
    ├── Feature 3: Password reset flow
    │   └── Test: Reset email sent, new password works
    │
    └── Feature 4: Role-based authorization
        └── Test: Admin routes blocked for regular users

Installation and Setup

Install Claude Code

macOS/Linux/WSL:

curl -fsSL https://claude.ai/install.sh | bash

Windows PowerShell:

irm https://claude.ai/install.ps1 | iex

Homebrew (macOS):

brew install claude-code

Verify Installation

claude --version

Start Using Claude Code

cd your-project
claude

You’ll be prompted to log in on first use. Requires a Claude subscription (Pro, Max, Teams, or Enterprise).

Terminal Recommendation

While the default terminal works, consider using a modern terminal like:

  • Ghostty – Fast, GPU-accelerated
  • Warp – AI-enhanced terminal
  • iTerm2 – macOS classic

Plan Mode and the AskUserQuestion Tool

Why Default Planning is Insufficient

When you enter Claude Code and use basic planning:

claude
# Press Shift+Tab to enter Plan Mode
> I want to build a GraphQL API for user management. Please help me create a plan.

Claude will ask 2-3 generic questions and start building. This is insufficient for production systems.

Default planning:

  • Asks superficial questions
  • Assumes things about architecture, error handling, database design
  • Doesn’t probe edge cases or tradeoffs
  • Results in rework when the finished product doesn’t match expectations

Deep Planning with AskUserQuestion

The AskUserQuestion tool forces Claude to interview you exhaustively before writing code. This is the most underutilized feature in Claude Code.

claude --permission-mode plan

Then use this prompt:

Read any existing plan files and interview me in detail using the AskUserQuestion tool
about literally anything: technical implementation, UI/UX, concerns, tradeoffs, etc.

Ask about:
- Database schema design and relationships
- Error handling strategies
- Authentication/authorization patterns
- API versioning approach
- Testing requirements
- Performance considerations
- Deployment constraints

Keep interviewing until we've covered everything, then write a complete specification
in SPECS.md

What AskUserQuestion Actually Asks

Round 1: Core Workflow and Technical Foundations

  • What’s your ideal workflow for this API from request to response?
  • How should the app handle API costs and rate limiting?
  • What database and hosting approach do you prefer?
  • How important is offline/local processing vs cloud-only?

Round 2: Architecture and Patterns

  • Do you want to use dependency injection with a custom DI container?
  • Which validation library? (Zod, Joi, class-validator)
  • How should errors be formatted for GraphQL clients?
  • Logging strategy? (Pino, Winston, structured JSON)

Round 3: Specific Implementation Details

  • JWT expiration times for access/refresh tokens?
  • Password hash rounds for bcrypt?
  • Rate limiting windows and thresholds?
  • Database connection pool sizes?

Practical Example: GraphQL API Project

Let’s walk through planning a real TypeScript + Express + GraphQL backend:

claude --permission-mode plan

Your Initial Prompt:

I want to build a production-ready GraphQL API for a SaaS application 
with user management, authentication, and basic CRUD operations.

Interview me using AskUserQuestion about:
- Tech stack decisions (TypeScript, Express, Apollo Server)
- Database design (PostgreSQL with native pg client)
- Authentication patterns (JWT with refresh tokens)
- Authorization (RBAC)
- Dependency injection pattern (custom DI Container)
- Error handling and validation
- Testing strategy (Jest, Supertest)
- Deployment considerations

Be thorough. I'd rather spend 30 minutes planning than 3 hours fixing.

Example Interview Flow:

Claude: What GraphQL server implementation do you prefer?
Options:
1. Apollo Server (most popular, rich ecosystem, subscriptions, caching)
2. express-graphql (lightweight, direct Express integration)
3. Mercurius (Fastify-based, high performance)
4. GraphQL Yoga (modern, Envelop-based)

You: Apollo Server - rich ecosystem, plugins, subscriptions, and Apollo Studio

Claude: For dependency injection, how do you want to handle 
service resolution?
Options:
1. Custom DI Container (lightweight, type-safe, no decorators)
2. InversifyJS (full-featured IoC container)
3. Awilix (function-based, no decorators)
4. Manual DI (constructor injection, no container)

You: Custom DI Container - lightweight, type-safe, no external dependencies

Claude: How should GraphQL errors be formatted?
Options:
1. Standard GraphQL errors only
2. Custom error codes with extensions
3. Full error class hierarchy (AuthError, ValidationError, etc.)
4. Union types for expected errors (Result pattern)

You: Custom error codes with extensions - clients need to 
handle different error types programmatically

After 15-20 questions, Claude produces a complete SPECS.md:

# User Management GraphQL API - SPECS
## Tech Stack
- Runtime: Node.js 20+
- Language: TypeScript 5+
- Framework: Express.js + Apollo Server
- Database: PostgreSQL 15+ (native pg client)
- DI Pattern: Custom DI Container (type-safe singleton/factory)
- Validation: Zod
- Authentication: jose (JWT), argon2 (password hashing)
- GraphQL Perf: DataLoader (N+1 prevention), graphql-scalars
- GraphQL Security: graphql-armor (query depth/complexity)
- Security: Helmet, CORS, express-rate-limit, Compression
- Logging: Pino
- Observability: OpenTelemetry (tracing), prom-client (metrics)
- Testing: Jest + Supertest + jest-mock-extended + Faker + Testcontainers
## Architecture Decisions
- Dependency injection via custom DI container
- Constructor injection (no decorators)
- Repositories receive dependencies via constructor, registered as singletons in DI container
## Authentication Flow
- Access Token: 15 minutes expiration, stored in memory
- Refresh Token: 7 days expiration, stored in httpOnly cookie
- Password: bcrypt with 12 rounds
## Error Handling
- Custom error classes extending ApolloError
- Error codes: AUTH_FAILED, VALIDATION_ERROR, NOT_FOUND, FORBIDDEN
- All errors logged with correlation ID (Pino)
- Apollo Server plugin for error formatting
## Features
- [ ] User registration with email validation
- [ ] Login/logout with JWT
- [ ] Token refresh endpoint
- [ ] User CRUD operations
- [ ] Role-based authorization (USER, ADMIN)
- [ ] Rate limiting (100 requests/15 minutes)
## Testing Requirements
- Unit tests for services (mocked repositories)
- Integration tests for resolvers (test database)
- E2E tests for critical flows (auth, user creation)

Build Without Ralph: Gaining Experience

«If you haven’t built anything, deployed anything, there’s not a URL that I myself can click on that you built—you don’t deserve to use Ralph.»

Why Build Manually First?

Learn to drive before buying a self-driving car:

  1. Understand the process: See how Claude interprets your instructions
  2. Develop debugging intuition: Know when Claude goes off track
  3. Learn to course correct: Master Esc, /rewind, and /clear
  4. Build quality instincts: Recognize good vs bad AI-generated code

Feature-by-Feature Development

With your SPECS.md ready, build one feature at a time:

claude

Feature 1: Database Schema

Let's start with the database schema from our SPECS.md. 

Create the SQL migration for the users table with:
- id (UUID, primary key)
- email (VARCHAR(255), unique, not null)
- password (VARCHAR(255), not null)
- name (VARCHAR(100), not null)
- role (user_role enum, not null, default 'user')
- is_active (boolean, not null, default true)
- created_at (TIMESTAMPTZ, default NOW())
- updated_at (TIMESTAMPTZ, default NOW())

Put it in migrations/001_create_users_table.sql following our project structure.
Also create the TypeScript model in src/models/user.model.ts

After Claude generates:

Run the SQL migration against local PostgreSQL to verify the schema works.
Show me the created table structure.

Feature 2: User Repository

Now create the UserRepository in src/repositories/user.repository.ts

It should have methods:
- findById(id: string): Promise<User | null>
- findByEmail(email: string): Promise<User | null>
- create(data: CreateUserDTO): Promise<User>
- update(id: string, data: UpdateUserDTO): Promise<User>
- delete(id: string): Promise<void>

Receive the database pool and logger via constructor injection.
Register UserRepository as a singleton in src/container/index.ts.
Extend BaseRepository if available (it also uses constructor injection).

Then verify:

Write a unit test for UserRepository in tests/integration/repositories/
Mock the database connection.
Run the test.

Test-Driven Approach

For each feature:

Example test-first workflow:

# In Claude Code
> Write a failing test for the AuthService.login method.
> It should:
>   - Return tokens for valid credentials
>   - Throw AuthError for invalid password
>   - Throw NotFoundError for non-existent user
>
> Put the test in tests/unit/services/auth.service.test.ts
> Use Jest and jest-mock-extended for proper mocking.
# Claude writes the test, you verify it fails
> Run the test to confirm it fails for the right reasons.
# Now implement
> Now implement AuthService.login to make all tests pass.
> Follow our error handling patterns from SPECS.md.
# Verify
> Run the tests again. Fix any failures.

Understanding Ralph Loops

The loop:

  1. Reads task list from SPECS.md
  2. Works on first incomplete task
  3. Writes tests for the feature
  4. Runs linting
  5. Documents progress in progress.txt
  6. Repeats until all tasks are complete

When to Use Ralph

✅ Use Ralph When❌ Don’t Use Ralph When
You have a battle-tested SPECSYou’re still exploring the idea
Tasks are clearly definedTasks are vague or ambiguous
You’ve built similar things beforeThis is your first project
You can verify results automaticallyVerification requires manual review
You’re comfortable with the codebaseYou’re learning the codebase

Configuring Ralph with Tests and Linting

A proper Ralph setup includes verification at every step:

# SPECS.md Structure for Ralph
## Summary
Build a minimal TypeScript Express server with GraphQL endpoints.
## Tasks
- [ ] Initialize project with TypeScript config
- [ ] Set up Express server with health endpoint
- [ ] Configure Apollo Server with basic schema
- [ ] Set up custom DI container
- [ ] Implement User type and queries
- [ ] Add createUser mutation with validation
- [ ] Write integration tests for GraphQL endpoints
- [ ] Add authentication middleware
- [ ] Document API in README
## Verification Requirements
After each task:
1. Run \`npm run type-check\`
2. Run \`npm run lint\`
3. Run \`npm run test\`
4. Everything must pass before moving to the next task

Ralph Loop Script Concept:

#!/bin/bash
# Simplified Ralph loop concept
while [ "\$(grep -c '\- \[ \]' SPECS.md)" -gt 0 ]; do
    # Get first incomplete task
    TASK=\$(grep -m1 '\- \[ \]' SPECS.md)
    
    # Run Claude on the task
    claude -p "Complete this task from SPECS.md: \$TASK. 
               After completing, run tests and lint.
               If tests pass, mark the task as complete in SPECS.md 
               and document in progress.txt.
               If tests fail, fix the issues."
    
    # Claude handles the rest internally
done

Note: Claude Code has a built-in Ralph plugin, but as the inventor suggests, building your own understanding first is more valuable than plug-and-play automation.


SPECS.md and Progress.txt

Your Project Documents

SPECS.md: The Single Source of Truth

# Project: User GraphQL API
## Summary
Production-ready GraphQL API for user management with JWT auth.
## Tech Stack
- **Runtime**: **Node.js 20+** — LTS stability, native `fetch`, modern Web APIs
- **Language**: **TypeScript 5+** — strict mode, latest type system features
- **Framework**: **Express + Apollo Server** — mature ecosystem, GraphQL subscriptions
- **Database**: **PostgreSQL 15+** — JSONB support, full-text search
- **DB Client**: **pg (node-postgres)** — full control, no ORM abstraction
- **DI Pattern**: **Custom DI Container** — type-safe, lightweight, no decorators
- **Validation**: **Zod** — runtime validation + static typing
- **Auth**: **jose (JWT) + argon2** — modern crypto, Web Crypto API, memory-hard hashing
- **GraphQL Performance**: **DataLoader** — batching and N+1 prevention
- **GraphQL Security**: **graphql-armor** — depth and complexity limits
- **Security**: **Helmet, CORS, express-rate-limit** — secure headers and rate limiting
- **Logging**: **Pino** — fast, structured JSON logs
- **Observability**: **OpenTelemetry, prom-client** — distributed tracing and metrics
- **Testing**: **Jest + Supertest + Testcontainers** — integration tests with real databases
## Architecture
src/
├── config/         # Environment, database, logger, telemetry
├── container/      # Custom DI container with type-safe resolvers
├── controllers/    # HTTP controllers (REST endpoints)
├── graphql/        # Apollo Server: schema, resolvers, context, loaders, plugins
│   └── loaders/    # DataLoaders for N+1 prevention
├── middleware/     # Auth, rate-limit, error handler
├── models/         # Database models and entities
├── repositories/   # Data access layer (constructor injection)
├── services/       # Business logic (constructor injection)
├── types/          # TypeScript definitions
├── utils/          # Helpers, errors
└── validators/     # Zod schemas
## Tasks
- [x] Project initialization with TypeScript
- [x] Database schema and migrations
- [x] Custom DI container setup
- [ ] User repository with constructor injection (registered in DI container)
- [ ] Auth service with constructor injection (registered in DI container)
- [ ] Apollo Server: GraphQL schema and resolvers (services resolved via DI context)
- [ ] Integration tests (using resolvers from DI container)
- [ ] Rate limiting middleware
- [ ] Docker configuration
## Constraints
- Response time: < 200ms for 95th percentile
- Test coverage: > 80%
- Zero critical security vulnerabilities

Progress.txt: Execution Log

# Progress Log

## 2026-01-29 10:30 - Project Initialization
✅ Created package.json with dependencies
✅ Configured tsconfig.json (strict mode)
✅ Jest configuration established
✅ Created src/ directory structure
Files: package.json, tsconfig.json, jest.config.ts

## 2026-01-29 11:15 - Database Schema
✅ Defined users table with SQL migration
✅ Created TypeScript User model
✅ Migration tested against local PostgreSQL
Files: src/models/user.model.ts, migrations/001_create_users_table.sql

## 2026-01-29 14:00 - User Repository with DI [IN PROGRESS]
⏳ Implementing CRUD operations with constructor injection
⏳ Registering UserRepository as singleton in DI container
⏳ Writing integration tests using resolvers.db()
Current file: src/repositories/user.repository.ts

Skills, MCP and CLAUDE.md

CLAUDE.md Configuration

Create a CLAUDE.md at the root of your project for persistent context:


# CLAUDE.md - Claude Code Project Configuration

> **This file is automatically loaded by Claude Code at the start of every session.**
> It provides essential context about project conventions, commands, and best practices.

---

## Quick Reference

### Build & Development Commands

```bash
npm run dev              # Start development server (tsx watch)
npm run build            # TypeScript compilation
npm run start            # Run production build
npm run type-check       # TypeScript type checking only
```

### Testing Commands

```bash
npm run test             # Run all tests
npm run test:watch       # Tests in watch mode
npm run test:coverage    # Tests with coverage report
npm run test:unit        # Unit tests only
npm run test:integration # Integration tests only
npm run test:e2e         # E2E tests only
npm test -- -t "name"    # Run single test by name
```

### Code Quality Commands

```bash
npm run lint             # ESLint check
npm run lint:fix         # ESLint auto-fix
npm run format           # Prettier formatting
```

### Database Commands

```bash
npm run db:migrate       # Run SQL migrations
npm run db:seed          # Seed with test data
```

### Docker Commands

```bash
npm run docker:dev       # Start dev environment with Docker Compose
npm run docker:down      # Stop and remove containers
```

---

## Core Philosophy

**Input Quality = Output Quality**

- Provide precise, detailed instructions (not vague requests)
- Think in **features**, not products (features are concrete and testable)
- Use the `AskUserQuestion` tool for thorough planning before coding
- Verify everything with tests, linting, and type checks

---

## Tech Stack

| Category | Technology | Notes |
|----------|------------|-------|
| Runtime | Node.js 20+ | LTS, native fetch |
| Language | TypeScript 5+ | Strict mode enabled |
| Framework | Express.js | HTTP layer |
| API Layer | Apollo Server (GraphQL) | Rich ecosystem, subscriptions |
| GraphQL Perf | DataLoader | N+1 prevention, batching |
| GraphQL Security | @escape.tech/graphql-armor | Query depth/complexity limits |
| DI Pattern | Custom DI Container | Type-safe singleton/factory, no decorators |
| Validation | Zod | Runtime + static types |
| Authentication | jose (JWT), argon2 | Modern JWT, memory-hard hashing |
| Security | Helmet, CORS, express-rate-limit | Secure headers, rate limiting |
| Performance | Compression | Response compression |
| Logging | Pino | Fast, structured JSON |
| Observability | OpenTelemetry, prom-client | Distributed tracing, metrics |
| Database | PostgreSQL 15+ | Native pg client |
| Type-Safe SQL | Kysely (optional) | Query builder |
| Testing | Jest, Supertest, jest-mock-extended, Faker, Testcontainers | Real DB in tests |
| Container | Docker | Multi-stage builds |

---

## Project Structure

```
src/
├── config/                 # Environment, database, logger, telemetry
│   ├── index.ts           # Zod-validated environment config
│   ├── database.ts        # PostgreSQL pool, transactions
│   └── logger.ts          # Pino logger setup
├── container/              # Dependency injection container
│   └── index.ts           # Type-safe DI with singleton/factory patterns
├── controllers/            # HTTP request handlers (REST endpoints)
├── graphql/                # GraphQL layer
│   ├── schema/            # Type definitions (SDL or code-first)
│   ├── resolvers/         # Query and mutation implementations
│   ├── loaders/           # DataLoaders for N+1 prevention
│   └── context.ts         # Request context with services and loaders
├── middleware/             # Express middleware
│   ├── auth.middleware.ts
│   ├── error-handler.middleware.ts
│   ├── validation.middleware.ts
│   └── request-logger.middleware.ts
├── models/                 # Database models and entities
├── repositories/           # Data access layer (pure SQL, no business logic)
├── services/               # Business logic layer
├── types/                  # TypeScript type definitions
├── utils/                  # Utility functions, custom errors
│   └── errors.ts          # AppError, ValidationError, NotFoundError, etc.
├── validators/             # Zod validation schemas
├── app.ts                  # Express app setup
└── server.ts               # Server entry point with graceful shutdown

tests/
├── unit/                   # Unit tests (mock dependencies)
├── integration/            # Integration tests (test database)
├── e2e/                    # End-to-end tests (full server)
├── fixtures/               # Test data factories
├── mocks/                  # Mock implementations
└── setup.ts                # Jest setup

docker/
├── Dockerfile              # Production multi-stage build
├── Dockerfile.dev          # Development with hot reload
└── docker-compose.yml      # Full dev environment

migrations/
└── *.sql                   # PostgreSQL migrations
```

---

## Architecture Rules

### Dependency Injection Principles

1. **Constructor Injection Only** → All dependencies are received via constructor parameters
2. **No Direct Instantiation** → Never use `new` for services/repositories in business code; always resolve from the DI container
3. **Container Manages Lifecycle** → Singletons (stateless services, repositories) and factories (request-scoped) are registered in `src/container/index.ts`
4. **Depend on Concrete Types** → TypeScript uses concrete classes (not interfaces) for DI; mocking in tests is done via `jest-mock-extended`

### Layer Responsibilities

1. **Resolvers/Controllers** → Receive services via DI context; call Services, never Repositories directly
2. **Services** → Business logic; receive repositories and other services via constructor injection
3. **Repositories** → Pure data access; receive database pool and logger via constructor injection
4. **Middleware** → Cross-cutting concerns; resolve services from DI container via `resolvers` helper

### Dependency Injection Pattern

Use **constructor injection** without decorators:

```typescript
// Services receive dependencies via constructor
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly logger: Logger
  ) {}
}

// Container registers singletons and factories
container.singleton(
  'userService',
  () => new UserService(
    container.resolve('userRepository'),
    container.resolve('logger')
  )
);
```

### Error Handling

All errors should extend `AppError` from `src/utils/errors.ts`:

```typescript
// Custom error classes
throw new NotFoundError('User');           // 404
throw new ValidationError('message', []);  // 400
throw new UnauthorizedError('message');    // 401
throw new ForbiddenError('message');       // 403
throw new ConflictError('message');        // 409
```

---

## Code Style Guidelines

### TypeScript

- Use **ES modules** (import/export), not CommonJS
- Prefer **named exports** over default exports
- All async functions must have **explicit return types**
- Use TypeScript **strict mode** (already configured)

### Validation

- Use **Zod** for all external input validation
- Validate at boundaries (controllers, resolvers, middleware)
- Transform and sanitize inputs in schemas

### Database

- All queries go through **repositories** (resolved from DI container)
- Use **parameterized queries** (prevent SQL injection)
- Use **transactions** for multi-step operations
- Prefer **DataLoader** for batched queries in GraphQL

### Testing

- **Unit tests**: Mock external dependencies with jest-mock-extended
- **Integration tests**: Use test database (Testcontainers recommended)
- **E2E tests**: Full server bootstrap
- Prefer running **individual tests** during development
- Target **80%+ coverage**

---

## Git Workflow

### Branch Naming

```
feature/   # New features
fix/       # Bug fixes
chore/     # Maintenance tasks
docs/      # Documentation updates
```

### Commit Messages (Conventional Commits)

```
feat: add user registration endpoint
fix: resolve token refresh race condition
docs: update API documentation
chore: upgrade dependencies
test: add auth service unit tests
```

### Pre-commit Checklist

```bash
npm run type-check && npm run lint && npm run test
```

---

## Environment Variables

Required in `.env`:

```env
# Server
NODE_ENV=development|test|production
PORT=3000

# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=appdb
DB_USER=postgres
DB_PASSWORD=postgres
DB_POOL_MIN=2
DB_POOL_MAX=10

# JWT (minimum 32 characters each)
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
JWT_REFRESH_SECRET=your-super-secret-refresh-key-min-32
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d

# Security
CORS_ORIGINS=http://localhost:3000,http://localhost:5173
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100

# Logging
LOG_LEVEL=debug|info|warn|error
```

---

## Planning Best Practices

### Use AskUserQuestion for Deep Planning

Before building any significant feature, use thorough planning:

```
Interview me using AskUserQuestion about:
- Database schema design and relationships
- Error handling strategies
- Authentication/authorization patterns
- API versioning approach
- Testing requirements
- Performance considerations
- Deployment constraints

Be thorough. Ask about tradeoffs. I'd rather answer 20 questions than rework code.
```

### SPECS.md Structure

Create a `SPECS.md` for project specifications:

```markdown
# Project: [Name]

## Summary
[One paragraph description]

## Tech Stack
[Table of technologies with justifications]

## Architecture
[Directory structure and layer responsibilities]

## Tasks
- [ ] Task 1
- [ ] Task 2

## Constraints
- Response time: < 200ms for 95th percentile
- Test coverage: > 80%
```

### Progress Tracking

Use `progress.txt` to document execution:

```text
## YYYY-MM-DD HH:MM - [Feature Name]
✅ Completed item 1
✅ Completed item 2
⏳ In progress: item 3
Files: file1.ts, file2.ts
```

---

## Context Management (50% Rule)

Claude's context degrades as it fills. Best practices:

1. **Start fresh sessions** for unrelated tasks
2. **Use `/clear`** between unrelated work
3. **Use `/compact`** when context grows large
4. **Delegate research** to subagents
5. **Watch for degradation**: Claude forgetting instructions, declining quality

### Useful Commands

| Command | Purpose |
|---------|---------|
| `/clear` | Reset context completely |
| `/compact [focus]` | Summarize and compress context |
| `/rewind` | Restore previous checkpoint |
| `Esc` | Stop current action |
| `Esc Esc` | Open rewind menu |

---

## Verification Checklist

After every feature implementation:

- [ ] `npm run type-check` passes
- [ ] `npm run lint` passes
- [ ] `npm run test` passes (or relevant test file)
- [ ] Manual verification if applicable
- [ ] Code follows architecture rules
- [ ] Error handling is complete
- [ ] Documentation updated if needed

---

## Security Checklist

- [ ] Use Helmet for security headers
- [ ] Configure CORS properly
- [ ] Hash passwords with argon2 (not bcrypt)
- [ ] Use short-lived JWTs (15m access, 7d refresh)
- [ ] Validate all inputs with Zod
- [ ] Sanitize error messages in production
- [ ] Use parameterized queries only
- [ ] Implement rate limiting
- [ ] Limit GraphQL query depth/complexity

---

## Quick Patterns

### Creating a New Service

```typescript
// 1. Define types in src/types/
// 2. Create Zod validators in src/validators/
// 3. Create repository in src/repositories/
// 4. Create service in src/services/
// 5. Register in DI container (src/container/index.ts)
// 6. Add resolver or controller
// 7. Write tests (unit + integration)
```

### Adding a GraphQL Resolver

1. Define schema types in `src/graphql/schema/`
2. Implement resolver in `src/graphql/resolvers/`
3. Add validation with Zod schemas
4. Create DataLoader if needed for batching
5. Write integration tests in `tests/integration/graphql/`

### Fixing a Bug

1. Write a **failing test** that reproduces the bug
2. Implement the fix
3. Verify test passes
4. Run full test suite
5. Commit with `fix:` prefix

---

## Resources

- [Apollo Server Docs](https://www.apollographql.com/docs/apollo-server/)
- [Zod Documentation](https://zod.dev/)
- [Jest Documentation](https://jestjs.io/docs/getting-started)
- [Testcontainers Guide](https://testcontainers.com/guides/getting-started-with-testcontainers-for-nodejs/)
- [OpenTelemetry JS](https://opentelemetry.io/docs/languages/js/)
- [jose JWT Library](https://github.com/panva/jose)
- [DataLoader](https://github.com/graphql/dataloader)
- [graphql-armor](https://escape.tech/graphql-armor/)

---

*Remember: Models are good enough now. If your output is garbage, your input was garbage. Invest in planning.*

Skills for Reusable Workflows

Create skills in .claude/skills/ for common tasks:


---
name: typescript-express-graphql-backend
description: Build production-ready TypeScript Node.js backend services with Express and Apollo Server GraphQL, implementing dependency injection pattern, JWT authentication, PostgreSQL integration, Docker containerization, and comprehensive testing. Use when creating scalable APIs, GraphQL servers, or microservices with enterprise-grade patterns.
---

# TypeScript Express GraphQL Backend Patterns

Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with Express, GraphQL, dependency injection pattern, and modern DevOps practices.

## When to Use This Skill

- Building REST APIs or GraphQL servers with TypeScript
- Creating microservices with Express and GraphQL
- Implementing JWT-based authentication and authorization
- Designing applications with dependency injection pattern
- Setting up PostgreSQL database integration
- Containerizing applications with Docker
- Writing comprehensive tests (unit, integration, E2E)
- Implementing middleware patterns and error handling

## Tech Stack Overview

| Category | Technology |
|----------|------------|
| Runtime | Node.js 20+ |
| Language | TypeScript 5+ |
| Framework | Express.js |
| API Layer | Apollo Server (GraphQL) |
| GraphQL Perf | DataLoader (N+1 prevention), graphql-scalars |
| GraphQL Security | @escape.tech/graphql-armor (query depth/complexity) |
| DI Pattern | Custom DI Container (type-safe singleton/factory) |
| Validation | Zod |
| Authentication | jose (JWT), argon2 (password hashing) |
| Security | Helmet, CORS, express-rate-limit |
| Performance | Compression |
| Logging | Pino |
| Observability | OpenTelemetry (tracing), prom-client (metrics) |
| Database | PostgreSQL |
| Type-Safe SQL | Kysely (optional) |
| Testing | Jest, Supertest, jest-mock-extended, Faker, @testcontainers/postgresql |
| Container | Docker |

## Project Structure

```
src/
├── config/                 # Configuration and environment
│   ├── index.ts
│   ├── database.ts
│   └── logger.ts
├── container/              # Dependency injection container
│   └── index.ts
├── controllers/            # HTTP request handlers (REST endpoints)
│   └── health.controller.ts
├── graphql/                # GraphQL layer
│   ├── schema/
│   │   ├── index.ts
│   │   ├── user.schema.ts
│   │   └── auth.schema.ts
│   ├── resolvers/
│   │   ├── index.ts
│   │   ├── user.resolver.ts
│   │   └── auth.resolver.ts
│   └── context.ts
├── middleware/             # Express middleware
│   ├── auth.middleware.ts
│   ├── error-handler.middleware.ts
│   ├── validation.middleware.ts
│   └── request-logger.middleware.ts
├── models/                 # Database models and entities
│   └── user.model.ts
├── repositories/           # Data access layer
│   ├── base.repository.ts
│   └── user.repository.ts
├── services/               # Business logic layer
│   ├── user.service.ts
│   └── auth.service.ts
├── types/                  # TypeScript type definitions
│   ├── index.ts
│   ├── user.types.ts
│   ├── auth.types.ts
│   └── express.d.ts
├── utils/                  # Utility functions and helpers
│   ├── errors.ts
│   ├── response.ts
│   └── password.ts
├── validators/             # Zod validation schemas
│   ├── user.validator.ts
│   └── auth.validator.ts
├── app.ts                  # Express app setup
└── server.ts               # Server entry point

tests/
├── unit/
│   ├── services/
│   └── utils/
├── integration/
│   ├── repositories/
│   └── graphql/
├── e2e/
│   └── api.test.ts
├── fixtures/
│   └── user.fixture.ts
├── mocks/
│   └── repositories.mock.ts
└── setup.ts

docker/
├── Dockerfile
├── Dockerfile.dev
└── docker-compose.yml

migrations/
└── 001_create_users_table.sql
```

## Configuration

### Environment Configuration

```typescript
// src/config/index.ts
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
  PORT: z.string().transform(Number).default('3000'),
  
  // Database
  DB_HOST: z.string(),
  DB_PORT: z.string().transform(Number).default('5432'),
  DB_NAME: z.string(),
  DB_USER: z.string(),
  DB_PASSWORD: z.string(),
  DB_POOL_MIN: z.string().transform(Number).default('2'),
  DB_POOL_MAX: z.string().transform(Number).default('10'),
  
  // JWT
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('15m'),
  JWT_REFRESH_SECRET: z.string().min(32),
  JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
  
  // Security
  CORS_ORIGINS: z.string().transform((val) => val.split(',')),
  RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('900000'),
  RATE_LIMIT_MAX: z.string().transform(Number).default('100'),
  
  // Logging
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
});

const parseEnv = () => {
  const result = envSchema.safeParse(process.env);
  
  if (!result.success) {
    console.error('❌ Invalid environment variables:');
    console.error(result.error.format());
    process.exit(1);
  }
  
  return result.data;
};

export const config = parseEnv();

export type Config = typeof config;
```

### Logger Configuration

```typescript
// src/config/logger.ts
import pino from 'pino';
import { config } from './index';

export const logger = pino({
  level: config.LOG_LEVEL,
  transport: config.NODE_ENV === 'development' 
    ? {
        target: 'pino-pretty',
        options: {
          colorize: true,
          translateTime: 'SYS:standard',
          ignore: 'pid,hostname',
        },
      }
    : undefined,
  base: {
    env: config.NODE_ENV,
  },
  redact: ['req.headers.authorization', 'password', 'token'],
});

export type Logger = typeof logger;
```

### Database Configuration

```typescript
// src/config/database.ts
import { Pool, PoolConfig, PoolClient } from 'pg';
import { config } from './index';
import { logger } from './logger';

const poolConfig: PoolConfig = {
  host: config.DB_HOST,
  port: config.DB_PORT,
  database: config.DB_NAME,
  user: config.DB_USER,
  password: config.DB_PASSWORD,
  min: config.DB_POOL_MIN,
  max: config.DB_POOL_MAX,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
};

export const pool = new Pool(poolConfig);

pool.on('connect', () => {
  logger.debug('New database connection established');
});

pool.on('error', (err) => {
  logger.error({ err }, 'Unexpected database pool error');
});

export const getClient = async (): Promise => {
  return pool.connect();
};

export const query = async (
  text: string,
  params?: any[]
): Promise => {
  const start = Date.now();
  const result = await pool.query(text, params);
  const duration = Date.now() - start;
  
  logger.debug({ query: text, duration, rows: result.rowCount }, 'Query executed');
  
  return result.rows;
};

export const transaction = async (
  callback: (client: PoolClient) => Promise
): Promise => {
  const client = await pool.connect();
  
  try {
    await client.query('BEGIN');
    const result = await callback(client);
    await client.query('COMMIT');
    return result;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
};

export const closeDatabase = async (): Promise => {
  await pool.end();
  logger.info('Database connections closed');
};

export const healthCheck = async (): Promise => {
  try {
    await pool.query('SELECT 1');
    return true;
  } catch {
    return false;
  }
};
```

## Dependency Injection Container

### Custom DI Container Pattern

A lightweight, type-safe DI container without external dependencies or decorators.

```typescript
// src/container/index.ts
import { Pool } from 'pg';
import { pool } from '../config/database';
import { logger, Logger } from '../config/logger';
import { config, Config } from '../config';

// Repositories
import { UserRepository } from '../repositories/user.repository';

// Services
import { UserService } from '../services/user.service';
import { AuthService } from '../services/auth.service';

// Controllers
import { UserController } from '../controllers/user.controller';

// Type-safe DI Container
class Container {
  private instances = new Map();

  register(key: string, factory: () => T): void {
    this.instances.set(key, factory);
  }

  resolve(key: string): T {
    const factory = this.instances.get(key);
    if (!factory) {
      throw new Error(`No factory registered for ${key}`);
    }
    return factory();
  }

  singleton(key: string, factory: () => T): void {
    let instance: T;
    this.instances.set(key, () => {
      if (!instance) {
        instance = factory();
      }
      return instance;
    });
  }
}

export const container = new Container();

// Register infrastructure (singletons)
container.singleton('db', () => pool);
container.singleton('logger', () => logger);
container.singleton('config', () => config);

// Register repositories (singletons - stateless, reusable)
container.singleton(
  'userRepository',
  () => new UserRepository(
    container.resolve('db'),
    container.resolve('logger')
  )
);

// Register services (singletons - stateless business logic)
container.singleton(
  'userService',
  () => new UserService(
    container.resolve('userRepository'),
    container.resolve('logger')
  )
);

container.singleton(
  'authService',
  () => new AuthService(
    container.resolve('userRepository'),
    container.resolve('config'),
    container.resolve('logger')
  )
);

// Register controllers (factory - new instance per request if needed)
container.register(
  'userController',
  () => new UserController(container.resolve('userService'))
);

// Type-safe resolver helpers
export const resolvers = {
  db: () => container.resolve('db'),
  logger: () => container.resolve('logger'),
  config: () => container.resolve('config'),
  userRepository: () => container.resolve('userRepository'),
  userService: () => container.resolve('userService'),
  authService: () => container.resolve('authService'),
  userController: () => container.resolve('userController'),
};
```

### Constructor Injection (No Decorators)

Classes receive dependencies through constructor parameters - pure DI without decorators.

```typescript
// src/repositories/user.repository.ts
import { Pool } from 'pg';
import { Logger } from '../config/logger';
import { User, CreateUserDTO, UpdateUserDTO } from '../types/user.types';
import { BaseRepository } from './base.repository';

export class UserRepository extends BaseRepository {
  constructor(
    private readonly db: Pool,
    private readonly logger: Logger
  ) {
    super(db, logger, 'users');
  }

  async findById(id: string): Promise {
    const result = await this.db.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result.rows[0] || null;
  }

  async findByEmail(email: string): Promise {
    const result = await this.db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );
    return result.rows[0] || null;
  }

  // ... other methods
}
```

```typescript
// src/services/auth.service.ts
import { UserRepository } from '../repositories/user.repository';
import { Config } from '../config';
import { Logger } from '../config/logger';
import { AuthTokens, LoginInput } from '../types/auth.types';

export class AuthService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly config: Config,
    private readonly logger: Logger
  ) {}

  async login(input: LoginInput): Promise {
    // Implementation uses this.userRepository, this.config, this.logger
  }

  // ... other methods
}
```

```typescript
// src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';

export class UserController {
  constructor(private readonly userService: UserService) {}

  async getUser(req: Request, res: Response, next: NextFunction): Promise {
    try {
      const user = await this.userService.getUserById(req.params.id);
      res.json({ status: 'success', data: user });
    } catch (error) {
      next(error);
    }
  }

  async createUser(req: Request, res: Response, next: NextFunction): Promise {
    try {
      const user = await this.userService.createUser(req.body);
      res.status(201).json({ status: 'success', data: user });
    } catch (error) {
      next(error);
    }
  }
}
```

## Type Definitions

### User Types

```typescript
// src/types/user.types.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: UserRole;
  isActive: boolean;
  createdAt: Date;
  updatedAt: Date;
}

export interface UserEntity extends User {
  password: string;
}

export type UserRole = 'user' | 'admin' | 'moderator';

export interface CreateUserDTO {
  email: string;
  password: string;
  name: string;
  role?: UserRole;
}

export interface UpdateUserDTO {
  email?: string;
  name?: string;
  role?: UserRole;
  isActive?: boolean;
}

export type UserWithoutPassword = Omit;
```

### Auth Types

```typescript
// src/types/auth.types.ts
import { UserRole } from './user.types';

export interface JWTPayload {
  userId: string;
  email: string;
  role: UserRole;
}

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
}

export interface LoginDTO {
  email: string;
  password: string;
}

export interface RegisterDTO {
  email: string;
  password: string;
  name: string;
}

export interface AuthResult {
  user: {
    id: string;
    email: string;
    name: string;
    role: UserRole;
  };
  tokens: TokenPair;
}

export interface RefreshTokenDTO {
  refreshToken: string;
}
```

### Express Type Extensions

```typescript
// src/types/express.d.ts
import { JWTPayload } from './auth.types';

declare global {
  namespace Express {
    interface Request {
      user?: JWTPayload;
      requestId?: string;
    }
  }
}

export {};
```

## Custom Errors

```typescript
// src/utils/errors.ts
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly isOperational: boolean;
  public readonly code: string;

  constructor(
    message: string,
    statusCode: number = 500,
    code: string = 'INTERNAL_ERROR',
    isOperational: boolean = true
  ) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = isOperational;

    Object.setPrototypeOf(this, AppError.prototype);
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends AppError {
  public readonly errors: Array<{ field: string; message: string }>;

  constructor(message: string, errors: Array<{ field: string; message: string }> = []) {
    super(message, 400, 'VALIDATION_ERROR');
    this.errors = errors;
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string = 'Resource') {
    super(\`\${resource} not found\`, 404, 'NOT_FOUND');
  }
}

export class UnauthorizedError extends AppError {
  constructor(message: string = 'Authentication required') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

export class ForbiddenError extends AppError {
  constructor(message: string = 'Access denied') {
    super(message, 403, 'FORBIDDEN');
  }
}

export class ConflictError extends AppError {
  constructor(message: string) {
    super(message, 409, 'CONFLICT');
  }
}

export class TooManyRequestsError extends AppError {
  constructor(message: string = 'Too many requests') {
    super(message, 429, 'TOO_MANY_REQUESTS');
  }
}

export const isAppError = (error: unknown): error is AppError => {
  return error instanceof AppError;
};
```

## Validators (Zod)

### User Validators

```typescript
// src/validators/user.validator.ts
import { z } from 'zod';

export const userRoleSchema = z.enum(['user', 'admin', 'moderator']);

export const createUserSchema = z.object({
  email: z
    .string()
    .email('Invalid email format')
    .max(255, 'Email must not exceed 255 characters')
    .transform((val) => val.toLowerCase().trim()),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .max(72, 'Password must not exceed 72 characters')
    .regex(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\$!%*?&])[A-Za-z\d@\$!%*?&]/,
      'Password must contain uppercase, lowercase, number, and special character'
    ),
  name: z
    .string()
    .min(1, 'Name is required')
    .max(100, 'Name must not exceed 100 characters')
    .trim(),
  role: userRoleSchema.optional().default('user'),
});

export const updateUserSchema = z.object({
  email: z
    .string()
    .email('Invalid email format')
    .max(255)
    .transform((val) => val.toLowerCase().trim())
    .optional(),
  name: z
    .string()
    .min(1)
    .max(100)
    .trim()
    .optional(),
  role: userRoleSchema.optional(),
  isActive: z.boolean().optional(),
});

export const userIdParamSchema = z.object({
  id: z.string().uuid('Invalid user ID format'),
});

export const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().positive().max(100).default(20),
  sortBy: z.string().optional().default('createdAt'),
  sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
});

export type CreateUserInput = z.infer;
export type UpdateUserInput = z.infer;
export type PaginationInput = z.infer;
```

### Auth Validators

```typescript
// src/validators/auth.validator.ts
import { z } from 'zod';

export const loginSchema = z.object({
  email: z
    .string()
    .email('Invalid email format')
    .transform((val) => val.toLowerCase().trim()),
  password: z.string().min(1, 'Password is required'),
});

export const registerSchema = z.object({
  email: z
    .string()
    .email('Invalid email format')
    .max(255)
    .transform((val) => val.toLowerCase().trim()),
  password: z
    .string()
    .min(8, 'Password must be at least 8 characters')
    .max(72, 'Password must not exceed 72 characters')
    .regex(
      /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@\$!%*?&])[A-Za-z\d@\$!%*?&]/,
      'Password must contain uppercase, lowercase, number, and special character'
    ),
  name: z
    .string()
    .min(1, 'Name is required')
    .max(100)
    .trim(),
});

export const refreshTokenSchema = z.object({
  refreshToken: z.string().min(1, 'Refresh token is required'),
});

export type LoginInput = z.infer;
export type RegisterInput = z.infer;
export type RefreshTokenInput = z.infer;
```

## Repository Layer (DI Pattern)

### Base Repository (Constructor Injection)

Base repository receives `db` pool and `logger` via constructor - all subclasses inherit this DI pattern.

```typescript
// src/repositories/base.repository.ts
import { Pool, PoolClient, QueryResult } from 'pg';
import { Logger } from '../config/logger';

export interface PaginationOptions {
  page: number;
  limit: number;
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
}

export interface PaginatedResult {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

export abstract class BaseRepository {
  constructor(
    protected readonly db: Pool,
    protected readonly logger: Logger,
    protected readonly tableName: string
  ) {}

  protected async query(
    text: string,
    params?: any[]
  ): Promise {
    const start = Date.now();
    const result = await this.db.query(text, params);
    const duration = Date.now() - start;

    this.logger.debug(
      { query: text, duration, rows: result.rowCount },
      'Query executed'
    );

    return result.rows;
  }

  protected async queryOne(
    text: string,
    params?: any[]
  ): Promise {
    const rows = await this.query(text, params);
    return rows[0] || null;
  }

  protected async execute(
    text: string,
    params?: any[]
  ): Promise {
    const result = await this.db.query(text, params);
    return result.rowCount ?? 0;
  }

  protected async withTransaction(
    callback: (client: PoolClient) => Promise
  ): Promise {
    const client = await this.db.connect();

    try {
      await client.query('BEGIN');
      const result = await callback(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  async findById(id: string): Promise {
    return this.queryOne(
      \`SELECT * FROM \${this.tableName} WHERE id = \$1\`,
      [id]
    );
  }

  async findAll(options: PaginationOptions): Promise> {
    const { page, limit, sortBy = 'created_at', sortOrder = 'desc' } = options;
    const offset = (page - 1) * limit;

    const allowedSortColumns = ['created_at', 'updated_at', 'name', 'email'];
    const safeSort = allowedSortColumns.includes(sortBy) ? sortBy : 'created_at';
    const safeOrder = sortOrder === 'asc' ? 'ASC' : 'DESC';

    const [data, countResult] = await Promise.all([
      this.query(
        \`SELECT * FROM \${this.tableName} 
         ORDER BY \${safeSort} \${safeOrder} 
         LIMIT \$1 OFFSET \$2\`,
        [limit, offset]
      ),
      this.queryOne<{ count: string }>(
        \`SELECT COUNT(*) as count FROM \${this.tableName}\`
      ),
    ]);

    const total = parseInt(countResult?.count || '0', 10);

    return {
      data,
      pagination: {
        page,
        limit,
        total,
        totalPages: Math.ceil(total / limit),
      },
    };
  }

  async delete(id: string): Promise {
    const rowCount = await this.execute(
      \`DELETE FROM \${this.tableName} WHERE id = \$1\`,
      [id]
    );
    return rowCount > 0;
  }
}
```

### User Repository

Repositories receive dependencies via constructor - registered as singletons in the DI container.

```typescript
// src/repositories/user.repository.ts
import { Pool } from 'pg';
import { Logger } from '../config/logger';
import { BaseRepository } from './base.repository';
import { UserEntity, CreateUserDTO, UpdateUserDTO } from '../types/user.types';

// Dependencies injected via constructor - resolved from container
export class UserRepository extends BaseRepository {
  constructor(
    private readonly db: Pool,
    private readonly logger: Logger
  ) {
    super(db, logger, 'users');
  }

  async create(data: CreateUserDTO & { password: string }): Promise {
    const result = await this.queryOne(
      \`INSERT INTO users (email, password, name, role)
       VALUES (\$1, \$2, \$3, \$4)
       RETURNING *\`,
      [data.email, data.password, data.name, data.role || 'user']
    );

    if (!result) {
      throw new Error('Failed to create user');
    }

    return result;
  }

  async findByEmail(email: string): Promise {
    return this.queryOne(
      'SELECT * FROM users WHERE email = \$1',
      [email.toLowerCase()]
    );
  }

  async update(id: string, data: UpdateUserDTO): Promise {
    const fields: string[] = [];
    const values: any[] = [];
    let paramIndex = 1;

    if (data.email !== undefined) {
      fields.push(\`email = \$\${paramIndex++}\`);
      values.push(data.email.toLowerCase());
    }
    if (data.name !== undefined) {
      fields.push(\`name = \$\${paramIndex++}\`);
      values.push(data.name);
    }
    if (data.role !== undefined) {
      fields.push(\`role = \$\${paramIndex++}\`);
      values.push(data.role);
    }
    if (data.isActive !== undefined) {
      fields.push(\`is_active = \$\${paramIndex++}\`);
      values.push(data.isActive);
    }

    if (fields.length === 0) {
      return this.findById(id);
    }

    fields.push(\`updated_at = CURRENT_TIMESTAMP\`);
    values.push(id);

    return this.queryOne(
      \`UPDATE users 
       SET \${fields.join(', ')} 
       WHERE id = \$\${paramIndex} 
       RETURNING *\`,
      values
    );
  }

  async existsByEmail(email: string): Promise {
    const result = await this.queryOne<{ exists: boolean }>(
      'SELECT EXISTS(SELECT 1 FROM users WHERE email = \$1) as exists',
      [email.toLowerCase()]
    );
    return result?.exists ?? false;
  }

  async findActiveUsers(): Promise {
    return this.query(
      'SELECT * FROM users WHERE is_active = true ORDER BY created_at DESC'
    );
  }

  async updatePassword(id: string, hashedPassword: string): Promise {
    const rowCount = await this.execute(
      'UPDATE users SET password = \$1, updated_at = CURRENT_TIMESTAMP WHERE id = \$2',
      [hashedPassword, id]
    );
    return rowCount > 0;
  }
}
```

## Service Layer

### Password Utility (argon2)

```typescript
// src/utils/password.ts
import argon2 from 'argon2';

// Argon2id - winner of Password Hashing Competition (PHC)
// Memory-hard, resistant to GPU/ASIC attacks
const ARGON2_OPTIONS: argon2.Options = {
  type: argon2.argon2id,
  memoryCost: 65536,  // 64 MB
  timeCost: 3,        // iterations
  parallelism: 4,
};

export const hashPassword = async (password: string): Promise => {
  return argon2.hash(password, ARGON2_OPTIONS);
};

export const comparePassword = async (
  password: string,
  hash: string
): Promise => {
  return argon2.verify(hash, password);
};
```

### User Service

Services receive dependencies via constructor - registered in the DI container.

```typescript
// src/services/user.service.ts
import { UserRepository } from '../repositories/user.repository';
import { Logger } from '../config/logger';
import {
  User,
  CreateUserDTO,
  UpdateUserDTO,
  UserWithoutPassword,
} from '../types/user.types';
import { PaginationOptions, PaginatedResult } from '../repositories/base.repository';
import { NotFoundError, ConflictError } from '../utils/errors';
import { hashPassword } from '../utils/password';

// Dependencies injected via constructor - no decorators needed
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly logger: Logger
  ) {}

  private sanitizeUser(user: User & { password?: string }): UserWithoutPassword {
    const { password, ...sanitized } = user;
    return sanitized as UserWithoutPassword;
  }

  async createUser(data: CreateUserDTO): Promise {
    this.logger.info({ email: data.email }, 'Creating new user');

    // Check if email already exists
    const exists = await this.userRepository.existsByEmail(data.email);
    if (exists) {
      throw new ConflictError('Email already registered');
    }

    // Hash password
    const hashedPassword = await hashPassword(data.password);

    // Create user
    const user = await this.userRepository.create({
      ...data,
      password: hashedPassword,
    });

    this.logger.info({ userId: user.id }, 'User created successfully');

    return this.sanitizeUser(user);
  }

  async getUserById(id: string): Promise {
    const user = await this.userRepository.findById(id);

    if (!user) {
      throw new NotFoundError('User');
    }

    return this.sanitizeUser(user);
  }

  async getUserByEmail(email: string): Promise {
    const user = await this.userRepository.findByEmail(email);
    return user ? this.sanitizeUser(user) : null;
  }

  async getUsers(options: PaginationOptions): Promise> {
    const result = await this.userRepository.findAll(options);

    return {
      ...result,
      data: result.data.map((user) => this.sanitizeUser(user)),
    };
  }

  async updateUser(id: string, data: UpdateUserDTO): Promise {
    this.logger.info({ userId: id }, 'Updating user');

    // Check if user exists
    const existingUser = await this.userRepository.findById(id);
    if (!existingUser) {
      throw new NotFoundError('User');
    }

    // Check email uniqueness if updating email
    if (data.email && data.email !== existingUser.email) {
      const emailExists = await this.userRepository.existsByEmail(data.email);
      if (emailExists) {
        throw new ConflictError('Email already in use');
      }
    }

    const updatedUser = await this.userRepository.update(id, data);

    if (!updatedUser) {
      throw new NotFoundError('User');
    }

    this.logger.info({ userId: id }, 'User updated successfully');

    return this.sanitizeUser(updatedUser);
  }

  async deleteUser(id: string): Promise {
    this.logger.info({ userId: id }, 'Deleting user');

    const deleted = await this.userRepository.delete(id);

    if (!deleted) {
      throw new NotFoundError('User');
    }

    this.logger.info({ userId: id }, 'User deleted successfully');
  }
}
```

### Auth Service (jose - Modern JWT)

Auth service uses constructor injection for all dependencies.

```typescript
// src/services/auth.service.ts
import * as jose from 'jose';
import { UserRepository } from '../repositories/user.repository';
import { Config } from '../config';
import { Logger } from '../config/logger';
import {
  JWTPayload,
  TokenPair,
  AuthResult,
  LoginDTO,
  RegisterDTO,
} from '../types/auth.types';
import { UnauthorizedError, ConflictError } from '../utils/errors';
import { hashPassword, comparePassword } from '../utils/password';

// Dependencies injected via constructor - resolved from DI container
// jose uses Web Crypto API - modern, typed, and secure
export class AuthService {
  private readonly accessSecret: Uint8Array;
  private readonly refreshSecret: Uint8Array;

  constructor(
    private readonly userRepository: UserRepository,
    private readonly config: Config,
    private readonly logger: Logger
  ) {
    // Convert secrets to Uint8Array for jose
    this.accessSecret = new TextEncoder().encode(this.config.JWT_SECRET);
    this.refreshSecret = new TextEncoder().encode(this.config.JWT_REFRESH_SECRET);
  }

  async register(data: RegisterDTO): Promise {
    this.logger.info({ email: data.email }, 'Registering new user');

    // Check if email exists
    const exists = await this.userRepository.existsByEmail(data.email);
    if (exists) {
      throw new ConflictError('Email already registered');
    }

    // Hash password and create user
    const hashedPassword = await hashPassword(data.password);
    const user = await this.userRepository.create({
      ...data,
      password: hashedPassword,
      role: 'user',
    });

    // Generate tokens
    const tokens = this.generateTokenPair({
      userId: user.id,
      email: user.email,
      role: user.role,
    });

    this.logger.info({ userId: user.id }, 'User registered successfully');

    return {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
      tokens,
    };
  }

  async login(data: LoginDTO): Promise {
    this.logger.info({ email: data.email }, 'User login attempt');

    // Find user by email
    const user = await this.userRepository.findByEmail(data.email);
    if (!user) {
      this.logger.warn({ email: data.email }, 'Login failed: user not found');
      throw new UnauthorizedError('Invalid credentials');
    }

    // Check if user is active
    if (!user.isActive) {
      this.logger.warn({ userId: user.id }, 'Login failed: user inactive');
      throw new UnauthorizedError('Account is deactivated');
    }

    // Verify password
    const isValid = await comparePassword(data.password, user.password);
    if (!isValid) {
      this.logger.warn({ userId: user.id }, 'Login failed: invalid password');
      throw new UnauthorizedError('Invalid credentials');
    }

    // Generate tokens
    const tokens = this.generateTokenPair({
      userId: user.id,
      email: user.email,
      role: user.role,
    });

    this.logger.info({ userId: user.id }, 'User logged in successfully');

    return {
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
      },
      tokens,
    };
  }

  async refreshTokens(refreshToken: string): Promise {
    try {
      const { payload } = await jose.jwtVerify(
        refreshToken,
        this.refreshSecret
      );

      // Verify user still exists and is active
      const user = await this.userRepository.findById(payload.userId as string);
      if (!user || !user.isActive) {
        throw new UnauthorizedError('Invalid refresh token');
      }

      // Generate new token pair
      return this.generateTokenPair({
        userId: user.id,
        email: user.email,
        role: user.role,
      });
    } catch (error) {
      if (error instanceof UnauthorizedError) {
        throw error;
      }
      this.logger.warn({ error }, 'Token refresh failed');
      throw new UnauthorizedError('Invalid refresh token');
    }
  }

  async verifyAccessToken(token: string): Promise {
    try {
      const { payload } = await jose.jwtVerify(token, this.accessSecret);
      return payload as unknown as JWTPayload;
    } catch (error) {
      throw new UnauthorizedError('Invalid access token');
    }
  }

  private async generateTokenPair(payload: JWTPayload): Promise {
    const accessToken = await new jose.SignJWT(payload as unknown as jose.JWTPayload)
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuedAt()
      .setExpirationTime(this.config.JWT_EXPIRES_IN)
      .sign(this.accessSecret);

    const refreshToken = await new jose.SignJWT(payload as unknown as jose.JWTPayload)
      .setProtectedHeader({ alg: 'HS256' })
      .setIssuedAt()
      .setExpirationTime(this.config.JWT_REFRESH_EXPIRES_IN)
      .sign(this.refreshSecret);

    return { accessToken, refreshToken };
  }
}
```

## Middleware

### Authentication Middleware

Middleware uses the DI container's `resolvers` to access services.

```typescript
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { resolvers } from '../container';
import { UnauthorizedError, ForbiddenError } from '../utils/errors';
import { UserRole } from '../types/user.types';

// Resolve AuthService from DI container
export const authenticate = async (
  req: Request,
  _res: Response,
  next: NextFunction
): Promise => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader?.startsWith('Bearer ')) {
      throw new UnauthorizedError('No token provided');
    }

    const token = authHeader.substring(7);
    
    // Get AuthService from DI container
    const authService = resolvers.authService();
    const payload = await authService.verifyAccessToken(token);

    req.user = payload;
    next();
  } catch (error) {
    next(error instanceof UnauthorizedError ? error : new UnauthorizedError('Invalid token'));
  }
};

export const authorize = (...allowedRoles: UserRole[]) => {
  return (req: Request, _res: Response, next: NextFunction): void => {
    if (!req.user) {
      return next(new UnauthorizedError('Authentication required'));
    }

    if (!allowedRoles.includes(req.user.role)) {
      return next(new ForbiddenError('Insufficient permissions'));
    }

    next();
  };
};

export const optionalAuth = (
  req: Request,
  _res: Response,
  next: NextFunction
): void => {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return next();
  }

  try {
    const token = authHeader.substring(7);
    const authService = resolvers.authService();
    req.user = authService.verifyAccessToken(token);
  } catch {
    // Ignore invalid tokens for optional auth
  }

  next();
};
```

### Validation Middleware

```typescript
// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AnyZodObject, ZodError } from 'zod';
import { ValidationError } from '../utils/errors';

type ValidateTarget = 'body' | 'query' | 'params';

interface ValidateOptions {
  body?: AnyZodObject;
  query?: AnyZodObject;
  params?: AnyZodObject;
}

export const validate = (schemas: ValidateOptions) => {
  return async (req: Request, _res: Response, next: NextFunction): Promise => {
    try {
      const errors: Array<{ field: string; message: string }> = [];

      for (const [target, schema] of Object.entries(schemas)) {
        if (schema) {
          const result = await schema.safeParseAsync(req[target as ValidateTarget]);
          
          if (!result.success) {
            errors.push(
              ...result.error.errors.map((err) => ({
                field: \`\${target}.\${err.path.join('.')}\`,
                message: err.message,
              }))
            );
          } else {
            // Replace with parsed/transformed values
            req[target as ValidateTarget] = result.data;
          }
        }
      }

      if (errors.length > 0) {
        throw new ValidationError('Validation failed', errors);
      }

      next();
    } catch (error) {
      if (error instanceof ValidationError) {
        next(error);
      } else if (error instanceof ZodError) {
        next(
          new ValidationError(
            'Validation failed',
            error.errors.map((err) => ({
              field: err.path.join('.'),
              message: err.message,
            }))
          )
        );
      } else {
        next(error);
      }
    }
  };
};
```

### Error Handler Middleware

```typescript
// src/middleware/error-handler.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AppError, ValidationError, isAppError } from '../utils/errors';
import { logger } from '../config/logger';

interface ErrorResponse {
  status: 'error';
  code: string;
  message: string;
  errors?: Array<{ field: string; message: string }>;
  stack?: string;
}

export const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  _next: NextFunction
): void => {
  const requestId = req.requestId;

  // Log error
  logger.error(
    {
      err,
      requestId,
      method: req.method,
      url: req.url,
      userId: req.user?.userId,
    },
    'Request error'
  );

  // Handle known application errors
  if (isAppError(err)) {
    const response: ErrorResponse = {
      status: 'error',
      code: err.code,
      message: err.message,
    };

    if (err instanceof ValidationError) {
      response.errors = err.errors;
    }

    res.status(err.statusCode).json(response);
    return;
  }

  // Handle unknown errors
  const statusCode = 500;
  const response: ErrorResponse = {
    status: 'error',
    code: 'INTERNAL_ERROR',
    message:
      process.env.NODE_ENV === 'production'
        ? 'An unexpected error occurred'
        : err.message,
  };

  // Include stack trace in development
  if (process.env.NODE_ENV !== 'production') {
    response.stack = err.stack;
  }

  res.status(statusCode).json(response);
};

export const notFoundHandler = (
  req: Request,
  res: Response,
  _next: NextFunction
): void => {
  res.status(404).json({
    status: 'error',
    code: 'NOT_FOUND',
    message: \`Route \${req.method} \${req.path} not found\`,
  });
};
```

### Request Logger Middleware

```typescript
// src/middleware/request-logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
import { logger } from '../config/logger';

export const requestLogger = (
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  const requestId = randomUUID();
  const startTime = Date.now();

  // Attach request ID
  req.requestId = requestId;
  res.setHeader('X-Request-ID', requestId);

  // Log request
  logger.info(
    {
      requestId,
      method: req.method,
      url: req.url,
      query: req.query,
      ip: req.ip,
      userAgent: req.get('user-agent'),
    },
    'Incoming request'
  );

  // Log response on finish
  res.on('finish', () => {
    const duration = Date.now() - startTime;

    logger.info(
      {
        requestId,
        method: req.method,
        url: req.url,
        statusCode: res.statusCode,
        duration: \`\${duration}ms\`,
        userId: req.user?.userId,
      },
      'Request completed'
    );
  });

  next();
};
```

### Rate Limiting Middleware

```typescript
// src/middleware/rate-limit.middleware.ts
import rateLimit from 'express-rate-limit';
import { TooManyRequestsError } from '../utils/errors';
import { logger } from '../config/logger';

// General API rate limiter
export const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
  handler: (req, _res, _next) => {
    logger.warn({ ip: req.ip, path: req.path }, 'Rate limit exceeded');
    throw new TooManyRequestsError('Too many requests, please try again later');
  },
});

// Strict limiter for auth endpoints
export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // 5 attempts per window
  standardHeaders: true,
  legacyHeaders: false,
  skipSuccessfulRequests: true, // Only count failed attempts
  handler: (req, _res, _next) => {
    logger.warn({ ip: req.ip }, 'Auth rate limit exceeded');
    throw new TooManyRequestsError('Too many login attempts, please try again later');
  },
});

// Strict limiter for password reset
export const passwordResetLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 3, // 3 requests per hour
  standardHeaders: true,
  legacyHeaders: false,
});
```

## GraphQL Layer

### DataLoader for N+1 Prevention

```typescript
// src/graphql/loaders/user.loader.ts
import DataLoader from 'dataloader';
import { Pool } from 'pg';
import { User } from '../../types/user.types';

// DataLoader batches individual findById calls into a single query
export const createUserLoader = (db: Pool) => {
  return new DataLoader(async (userIds) => {
    const result = await db.query(
      'SELECT * FROM users WHERE id = ANY($1)',
      [userIds]
    );

    // Map results back to the order of input IDs
    const userMap = new Map(result.rows.map((user) => [user.id, user]));
    return userIds.map((id) => userMap.get(id) ?? null);
  });
};

// Batch load users by email
export const createUserByEmailLoader = (db: Pool) => {
  return new DataLoader(async (emails) => {
    const result = await db.query(
      'SELECT * FROM users WHERE email = ANY($1)',
      [emails]
    );

    const emailMap = new Map(result.rows.map((user) => [user.email, user]));
    return emails.map((email) => emailMap.get(email) ?? null);
  });
};
```

### GraphQL Context with DataLoaders

Context uses the DI container's `resolvers` to inject services into GraphQL resolvers.

```typescript
// src/graphql/context.ts
import { Request, Response } from 'express';
import DataLoader from 'dataloader';
import { JWTPayload } from '../types/auth.types';
import { resolvers } from '../container';
import { UserService } from '../services/user.service';
import { AuthService } from '../services/auth.service';
import { Logger } from '../config/logger';
import { createUserLoader, createUserByEmailLoader } from './loaders/user.loader';
import { User } from '../types/user.types';

export interface GraphQLContext {
  req: Request;
  res: Response;
  user?: JWTPayload;
  services: {
    userService: UserService;
    authService: AuthService;
  };
  loaders: {
    userLoader: DataLoader;
    userByEmailLoader: DataLoader;
  };
  logger: Logger;
}

export const createContext = (req: Request, res: Response): GraphQLContext => {
  // DataLoaders are created per-request to enable batching within a request
  // and avoid caching across different users
  // Services are resolved from the DI container (singletons)
  const db = resolvers.db();
  
  return {
    req,
    res,
    user: req.user,
    services: {
      // Resolve services from DI container
      userService: resolvers.userService(),
      authService: resolvers.authService(),
    },
    loaders: {
      // DataLoaders use the db pool from DI container
      userLoader: createUserLoader(db),
      userByEmailLoader: createUserByEmailLoader(db),
    },
    logger: resolvers.logger(),
  };
};
```

### GraphQL Schema

```typescript
// src/graphql/schema/index.ts
import {
  GraphQLSchema,
  GraphQLObjectType,
} from 'graphql';
import { 
  queryFields as userQueryFields, 
  mutationFields as userMutationFields 
} from './user.schema';
import { 
  queryFields as authQueryFields, 
  mutationFields as authMutationFields 
} from './auth.schema';

const QueryType = new GraphQLObjectType({
  name: 'Query',
  fields: () => ({
    ...userQueryFields,
    ...authQueryFields,
  }),
});

const MutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: () => ({
    ...userMutationFields,
    ...authMutationFields,
  }),
});

export const schema = new GraphQLSchema({
  query: QueryType,
  mutation: MutationType,
});
```

### User Schema

```typescript
// src/graphql/schema/user.schema.ts
import {
  GraphQLObjectType,
  GraphQLString,
  GraphQLNonNull,
  GraphQLList,
  GraphQLInt,
  GraphQLBoolean,
  GraphQLEnumType,
  GraphQLInputObjectType,
  GraphQLFieldConfigMap,
} from 'graphql';
import { GraphQLContext } from '../context';
import { userResolvers } from '../resolvers/user.resolver';

export const UserRoleEnum = new GraphQLEnumType({
  name: 'UserRole',
  values: {
    USER: { value: 'user' },
    ADMIN: { value: 'admin' },
    MODERATOR: { value: 'moderator' },
  },
});

export const UserType = new GraphQLObjectType({
  name: 'User',
  fields: () => ({
    id: { type: new GraphQLNonNull(GraphQLString) },
    email: { type: new GraphQLNonNull(GraphQLString) },
    name: { type: new GraphQLNonNull(GraphQLString) },
    role: { type: new GraphQLNonNull(UserRoleEnum) },
    isActive: { type: new GraphQLNonNull(GraphQLBoolean) },
    createdAt: { type: new GraphQLNonNull(GraphQLString) },
    updatedAt: { type: new GraphQLNonNull(GraphQLString) },
  }),
});

export const PaginationInfoType = new GraphQLObjectType({
  name: 'PaginationInfo',
  fields: () => ({
    page: { type: new GraphQLNonNull(GraphQLInt) },
    limit: { type: new GraphQLNonNull(GraphQLInt) },
    total: { type: new GraphQLNonNull(GraphQLInt) },
    totalPages: { type: new GraphQLNonNull(GraphQLInt) },
  }),
});

export const UsersConnectionType = new GraphQLObjectType({
  name: 'UsersConnection',
  fields: () => ({
    data: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UserType))) },
    pagination: { type: new GraphQLNonNull(PaginationInfoType) },
  }),
});

const UpdateUserInputType = new GraphQLInputObjectType({
  name: 'UpdateUserInput',
  fields: () => ({
    email: { type: GraphQLString },
    name: { type: GraphQLString },
    role: { type: UserRoleEnum },
    isActive: { type: GraphQLBoolean },
  }),
});

export const queryFields: GraphQLFieldConfigMap = {
  user: {
    type: UserType,
    args: {
      id: { type: new GraphQLNonNull(GraphQLString) },
    },
    resolve: userResolvers.getUser,
  },
  users: {
    type: new GraphQLNonNull(UsersConnectionType),
    args: {
      page: { type: GraphQLInt, defaultValue: 1 },
      limit: { type: GraphQLInt, defaultValue: 20 },
      sortBy: { type: GraphQLString, defaultValue: 'createdAt' },
      sortOrder: { type: GraphQLString, defaultValue: 'desc' },
    },
    resolve: userResolvers.getUsers,
  },
  me: {
    type: UserType,
    resolve: userResolvers.me,
  },
};

export const mutationFields: GraphQLFieldConfigMap = {
  updateUser: {
    type: UserType,
    args: {
      id: { type: new GraphQLNonNull(GraphQLString) },
      input: { type: new GraphQLNonNull(UpdateUserInputType) },
    },
    resolve: userResolvers.updateUser,
  },
  deleteUser: {
    type: GraphQLBoolean,
    args: {
      id: { type: new GraphQLNonNull(GraphQLString) },
    },
    resolve: userResolvers.deleteUser,
  },
};
```

### Auth Schema

```typescript
// src/graphql/schema/auth.schema.ts
import {
  GraphQLObjectType,
  GraphQLString,
  GraphQLNonNull,
  GraphQLInputObjectType,
  GraphQLFieldConfigMap,
} from 'graphql';
import { GraphQLContext } from '../context';
import { UserType } from './user.schema';
import { authResolvers } from '../resolvers/auth.resolver';

export const TokensType = new GraphQLObjectType({
  name: 'Tokens',
  fields: () => ({
    accessToken: { type: new GraphQLNonNull(GraphQLString) },
    refreshToken: { type: new GraphQLNonNull(GraphQLString) },
  }),
});

export const AuthResultType = new GraphQLObjectType({
  name: 'AuthResult',
  fields: () => ({
    user: { type: new GraphQLNonNull(UserType) },
    tokens: { type: new GraphQLNonNull(TokensType) },
  }),
});

const RegisterInputType = new GraphQLInputObjectType({
  name: 'RegisterInput',
  fields: () => ({
    email: { type: new GraphQLNonNull(GraphQLString) },
    password: { type: new GraphQLNonNull(GraphQLString) },
    name: { type: new GraphQLNonNull(GraphQLString) },
  }),
});

const LoginInputType = new GraphQLInputObjectType({
  name: 'LoginInput',
  fields: () => ({
    email: { type: new GraphQLNonNull(GraphQLString) },
    password: { type: new GraphQLNonNull(GraphQLString) },
  }),
});

export const queryFields: GraphQLFieldConfigMap = {};

export const mutationFields: GraphQLFieldConfigMap = {
  register: {
    type: new GraphQLNonNull(AuthResultType),
    args: {
      input: { type: new GraphQLNonNull(RegisterInputType) },
    },
    resolve: authResolvers.register,
  },
  login: {
    type: new GraphQLNonNull(AuthResultType),
    args: {
      input: { type: new GraphQLNonNull(LoginInputType) },
    },
    resolve: authResolvers.login,
  },
  refreshTokens: {
    type: new GraphQLNonNull(TokensType),
    args: {
      refreshToken: { type: new GraphQLNonNull(GraphQLString) },
    },
    resolve: authResolvers.refreshTokens,
  },
};
```

### User Resolver

```typescript
// src/graphql/resolvers/user.resolver.ts
import { GraphQLContext } from '../context';
import { UnauthorizedError, ForbiddenError } from '../../utils/errors';
import { updateUserSchema, paginationSchema, userIdParamSchema } from '../../validators/user.validator';

const requireAuth = (context: GraphQLContext): void => {
  if (!context.user) {
    throw new UnauthorizedError('Authentication required');
  }
};

const requireAdmin = (context: GraphQLContext): void => {
  requireAuth(context);
  if (context.user?.role !== 'admin') {
    throw new ForbiddenError('Admin access required');
  }
};

export const userResolvers = {
  getUser: async (
    _parent: any,
    args: { id: string },
    context: GraphQLContext
  ) => {
    requireAuth(context);
    
    const { id } = userIdParamSchema.parse(args);
    return context.services.userService.getUserById(id);
  },

  getUsers: async (
    _parent: any,
    args: { page?: number; limit?: number; sortBy?: string; sortOrder?: string },
    context: GraphQLContext
  ) => {
    requireAdmin(context);
    
    const pagination = paginationSchema.parse(args);
    return context.services.userService.getUsers(pagination);
  },

  me: async (
    _parent: any,
    _args: any,
    context: GraphQLContext
  ) => {
    requireAuth(context);
    return context.services.userService.getUserById(context.user!.userId);
  },

  updateUser: async (
    _parent: any,
    args: { id: string; input: any },
    context: GraphQLContext
  ) => {
    requireAuth(context);
    
    const { id } = userIdParamSchema.parse({ id: args.id });
    
    // Users can only update themselves, unless admin
    if (context.user?.userId !== id && context.user?.role !== 'admin') {
      throw new ForbiddenError('Cannot update other users');
    }
    
    const input = updateUserSchema.parse(args.input);
    
    // Non-admins cannot change roles
    if (input.role && context.user?.role !== 'admin') {
      throw new ForbiddenError('Cannot change user role');
    }
    
    return context.services.userService.updateUser(id, input);
  },

  deleteUser: async (
    _parent: any,
    args: { id: string },
    context: GraphQLContext
  ) => {
    requireAdmin(context);
    
    const { id } = userIdParamSchema.parse(args);
    await context.services.userService.deleteUser(id);
    return true;
  },
};
```

### Auth Resolver

```typescript
// src/graphql/resolvers/auth.resolver.ts
import { GraphQLContext } from '../context';
import { loginSchema, registerSchema, refreshTokenSchema } from '../../validators/auth.validator';

export const authResolvers = {
  register: async (
    _parent: any,
    args: { input: any },
    context: GraphQLContext
  ) => {
    const input = registerSchema.parse(args.input);
    return context.services.authService.register(input);
  },

  login: async (
    _parent: any,
    args: { input: any },
    context: GraphQLContext
  ) => {
    const input = loginSchema.parse(args.input);
    return context.services.authService.login(input);
  },

  refreshTokens: async (
    _parent: any,
    args: { refreshToken: string },
    context: GraphQLContext
  ) => {
    const { refreshToken } = refreshTokenSchema.parse(args);
    return context.services.authService.refreshTokens(refreshToken);
  },
};
```

## Express Application Setup

Application setup imports the DI container and uses resolvers for service access.

```typescript
// src/app.ts
import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import http from 'http';

import { config } from './config';
import { schema } from './graphql/schema';
import { createContext, Context } from './graphql/context';
import { requestLogger } from './middleware/request-logger.middleware';
import { errorHandler, notFoundHandler } from './middleware/error-handler.middleware';
import { healthCheck } from './config/database';
import { resolvers } from './container';

export const createApp = async (): Promise<{ app: Application; server: ApolloServer }> => {
  const app = express();
  const httpServer = http.createServer(app);

  // Security middleware
  app.use(
    helmet({
      contentSecurityPolicy: config.NODE_ENV === 'production' ? undefined : false,
    })
  );
  
  app.use(
    cors({
      origin: config.CORS_ORIGINS,
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
      allowedHeaders: ['Content-Type', 'Authorization'],
    })
  );

  // Compression
  app.use(compression());

  // Body parsing
  app.use(express.json({ limit: '10mb' }));
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));

  // Request logging
  app.use(requestLogger);

  // Health check endpoint
  app.get('/health', async (_req, res) => {
    const dbHealthy = await healthCheck();
    const status = dbHealthy ? 'healthy' : 'unhealthy';
    const statusCode = dbHealthy ? 200 : 503;

    res.status(statusCode).json({
      status,
      timestamp: new Date().toISOString(),
      services: {
        database: dbHealthy ? 'up' : 'down',
      },
    });
  });

  // Ready check endpoint
  app.get('/ready', (_req, res) => {
    res.json({ status: 'ready' });
  });

  // Apollo Server setup
  const apolloServer = new ApolloServer({
    schema,
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer }),
      {
        async requestDidStart() {
          return {
            async didEncounterErrors(requestContext) {
              // Log errors with correlation ID
              requestContext.errors?.forEach((error) => {
                console.error('GraphQL Error:', {
                  message: error.message,
                  code: error.extensions?.code,
                  path: error.path,
                });
              });
            },
          };
        },
      },
    ],
    formatError: (formattedError, error) => {
      return {
        message: formattedError.message,
        code: formattedError.extensions?.code || 'GRAPHQL_ERROR',
        locations: formattedError.locations,
        path: formattedError.path,
        ...(config.NODE_ENV !== 'production' && { 
          stack: (error as Error).stack 
        }),
      };
    },
    introspection: config.NODE_ENV === 'development',
  });

  await apolloServer.start();

  // GraphQL endpoint with Apollo Server middleware
  app.use(
    '/graphql',
    expressMiddleware(apolloServer, {
      context: async ({ req, res }) => createContext(req, res, container),
    })
  );

  // 404 handler
  app.use(notFoundHandler);

  // Global error handler
  app.use(errorHandler);

  return { app, server: apolloServer };
};
```

### Server Entry Point

```typescript
// src/server.ts
import { createApp } from './app';
import { config } from './config';
import { logger } from './config/logger';
import { closeDatabase } from './config/database';

const app = createApp();

const server = app.listen(config.PORT, () => {
  logger.info(
    {
      port: config.PORT,
      env: config.NODE_ENV,
    },
    '🚀 Server started'
  );
});

// Graceful shutdown
const gracefulShutdown = async (signal: string) => {
  logger.info({ signal }, 'Received shutdown signal');

  server.close(async () => {
    logger.info('HTTP server closed');

    try {
      await closeDatabase();
      logger.info('Graceful shutdown completed');
      process.exit(0);
    } catch (error) {
      logger.error({ error }, 'Error during graceful shutdown');
      process.exit(1);
    }
  });

  // Force shutdown after timeout
  setTimeout(() => {
    logger.error('Forced shutdown due to timeout');
    process.exit(1);
  }, 30000);
};

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
  logger.fatal({ error }, 'Uncaught exception');
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.fatal({ reason, promise }, 'Unhandled rejection');
  process.exit(1);
});
```

## Docker Configuration

### Dockerfile (Production)

```dockerfile
# docker/Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production=false

# Copy source and build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

# Production stage
FROM node:20-alpine AS production

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# Copy production dependencies
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# Copy built application
COPY --from=builder /app/dist ./dist

# Set ownership
RUN chown -R nodejs:nodejs /app

USER nodejs

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000

CMD ["node", "dist/server.js"]
```

### Dockerfile (Development)

```dockerfile
# docker/Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Copy source
COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]
```

### Docker Compose

```yaml
# docker/docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: docker/Dockerfile.dev
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=development
      - PORT=3000
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_NAME=appdb
      - DB_USER=postgres
      - DB_PASSWORD=postgres
      - JWT_SECRET=your-super-secret-jwt-key-min-32-chars
      - JWT_REFRESH_SECRET=your-super-secret-refresh-key-min-32
      - CORS_ORIGINS=http://localhost:3000,http://localhost:5173
      - LOG_LEVEL=debug
    volumes:
      - ../src:/app/src
      - ../package.json:/app/package.json
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network

  postgres:
    image: postgres:16-alpine
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_DB=appdb
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ../migrations:/docker-entrypoint-initdb.d
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres -d appdb']
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  postgres-data:

networks:
  app-network:
    driver: bridge
```

### Database Migration

```sql
-- migrations/001_create_users_table.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TYPE user_role AS ENUM ('user', 'admin', 'moderator');

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    name VARCHAR(100) NOT NULL,
    role user_role NOT NULL DEFAULT 'user',
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_is_active ON users(is_active);

-- Trigger to update updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS \$\$
BEGIN
    NEW.updated_at = CURRENT_TIMESTAMP;
    RETURN NEW;
END;
\$\$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at
    BEFORE UPDATE ON users
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at_column();
```

## Testing Patterns

### Jest Configuration

```typescript
// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['/tests', '/src'],
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '/src/$1',
  },
  setupFilesAfterEnv: ['/tests/setup.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/server.ts',
    '!src/types/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  coverageReporters: ['text', 'lcov', 'html'],
  verbose: true,
  testTimeout: 30000,
};

export default config;
```

### Test Setup

```typescript
// tests/setup.ts
import { container } from '../src/container';

// Clear container before each test
beforeEach(() => {
  jest.clearAllMocks();
});

// Global test timeout
jest.setTimeout(30000);

// Suppress console during tests
if (process.env.SUPPRESS_LOGS === 'true') {
  global.console = {
    ...console,
    log: jest.fn(),
    debug: jest.fn(),
    info: jest.fn(),
    warn: jest.fn(),
  };
}
```

### Test Fixtures

```typescript
// tests/fixtures/user.fixture.ts
import { faker } from '@faker-js/faker';
import { User, UserEntity, CreateUserDTO, UserRole } from '../../src/types/user.types';

export const createUserFixture = (overrides: Partial = {}): User => ({
  id: faker.string.uuid(),
  email: faker.internet.email().toLowerCase(),
  name: faker.person.fullName(),
  role: 'user' as UserRole,
  isActive: true,
  createdAt: new Date(),
  updatedAt: new Date(),
  ...overrides,
});

export const createUserEntityFixture = (overrides: Partial = {}): UserEntity => ({
  ...createUserFixture(),
  password: '\$2b\$12\$hashedpassword',
  ...overrides,
});

export const createUserDTOFixture = (overrides: Partial = {}): CreateUserDTO => ({
  email: faker.internet.email().toLowerCase(),
  password: 'ValidP@ss123',
  name: faker.person.fullName(),
  ...overrides,
});
```

### Mock Repositories

```typescript
// tests/mocks/repositories.mock.ts
import { mock, MockProxy } from 'jest-mock-extended';
import { UserRepository } from '../../src/repositories/user.repository';
import { Pool } from 'pg';
import { Logger } from '../../src/config/logger';

export const createMockUserRepository = (): MockProxy => {
  return mock();
};

export const createMockPool = (): MockProxy => {
  return mock();
};

export const createMockLogger = (): MockProxy => {
  return mock();
};
```

### Unit Tests

With DI pattern, unit tests inject mock dependencies directly via constructor.

```typescript
// tests/unit/services/user.service.test.ts
import { MockProxy } from 'jest-mock-extended';
import { UserService } from '../../../src/services/user.service';
import { UserRepository } from '../../../src/repositories/user.repository';
import { Logger } from '../../../src/config/logger';
import { createMockUserRepository, createMockLogger } from '../../mocks/repositories.mock';
import { createUserEntityFixture, createUserDTOFixture } from '../../fixtures/user.fixture';
import { NotFoundError, ConflictError } from '../../../src/utils/errors';
import * as passwordUtils from '../../../src/utils/password';

jest.mock('../../../src/utils/password');

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepository: MockProxy;
  let mockLogger: MockProxy;

  beforeEach(() => {
    // Create mock dependencies
    mockUserRepository = createMockUserRepository();
    mockLogger = createMockLogger();
    
    // Inject mocks via constructor - same pattern as DI container
    userService = new UserService(mockUserRepository, mockLogger);
    
    (passwordUtils.hashPassword as jest.Mock).mockResolvedValue('hashedPassword');
  });

  describe('createUser', () => {
    it('should create a new user successfully', async () => {
      const dto = createUserDTOFixture();
      const createdUser = createUserEntityFixture({ email: dto.email, name: dto.name });

      mockUserRepository.existsByEmail.mockResolvedValue(false);
      mockUserRepository.create.mockResolvedValue(createdUser);

      const result = await userService.createUser(dto);

      expect(result).not.toHaveProperty('password');
      expect(result.email).toBe(dto.email);
      expect(mockUserRepository.existsByEmail).toHaveBeenCalledWith(dto.email);
      expect(mockUserRepository.create).toHaveBeenCalled();
    });

    it('should throw ConflictError if email exists', async () => {
      const dto = createUserDTOFixture();
      mockUserRepository.existsByEmail.mockResolvedValue(true);

      await expect(userService.createUser(dto)).rejects.toThrow(ConflictError);
    });
  });

  describe('getUserById', () => {
    it('should return user without password', async () => {
      const user = createUserEntityFixture();
      mockUserRepository.findById.mockResolvedValue(user);

      const result = await userService.getUserById(user.id);

      expect(result).not.toHaveProperty('password');
      expect(result.id).toBe(user.id);
    });

    it('should throw NotFoundError if user not found', async () => {
      mockUserRepository.findById.mockResolvedValue(null);

      await expect(userService.getUserById('non-existent')).rejects.toThrow(NotFoundError);
    });
  });

  describe('deleteUser', () => {
    it('should delete user successfully', async () => {
      mockUserRepository.delete.mockResolvedValue(true);

      await expect(userService.deleteUser('user-id')).resolves.not.toThrow();
    });

    it('should throw NotFoundError if user not found', async () => {
      mockUserRepository.delete.mockResolvedValue(false);

      await expect(userService.deleteUser('non-existent')).rejects.toThrow(NotFoundError);
    });
  });
});
```

### Auth Service Tests

```typescript
// tests/unit/services/auth.service.test.ts
import { MockProxy } from 'jest-mock-extended';
import { AuthService } from '../../../src/services/auth.service';
import { UserRepository } from '../../../src/repositories/user.repository';
import { Config } from '../../../src/config';
import { Logger } from '../../../src/config/logger';
import { createMockUserRepository, createMockLogger } from '../../mocks/repositories.mock';
import { createUserEntityFixture } from '../../fixtures/user.fixture';
import { UnauthorizedError, ConflictError } from '../../../src/utils/errors';
import * as passwordUtils from '../../../src/utils/password';

jest.mock('../../../src/utils/password');

describe('AuthService', () => {
  let authService: AuthService;
  let mockUserRepository: MockProxy;
  let mockLogger: MockProxy;
  let mockConfig: Config;

  beforeEach(() => {
    mockUserRepository = createMockUserRepository();
    mockLogger = createMockLogger();
    mockConfig = {
      JWT_SECRET: 'test-secret-key-with-32-characters!',
      JWT_REFRESH_SECRET: 'test-refresh-secret-key-32-chars!',
      JWT_EXPIRES_IN: '15m',
      JWT_REFRESH_EXPIRES_IN: '7d',
    } as Config;

    authService = new AuthService(mockUserRepository, mockConfig, mockLogger);
    
    (passwordUtils.hashPassword as jest.Mock).mockResolvedValue('hashedPassword');
    (passwordUtils.comparePassword as jest.Mock).mockResolvedValue(true);
  });

  describe('login', () => {
    it('should return tokens and user on successful login', async () => {
      const user = createUserEntityFixture();
      mockUserRepository.findByEmail.mockResolvedValue(user);

      const result = await authService.login({
        email: user.email,
        password: 'password',
      });

      expect(result.user.id).toBe(user.id);
      expect(result.tokens.accessToken).toBeDefined();
      expect(result.tokens.refreshToken).toBeDefined();
    });

    it('should throw UnauthorizedError for invalid credentials', async () => {
      mockUserRepository.findByEmail.mockResolvedValue(null);

      await expect(
        authService.login({ email: 'test@test.com', password: 'wrong' })
      ).rejects.toThrow(UnauthorizedError);
    });

    it('should throw UnauthorizedError for inactive user', async () => {
      const user = createUserEntityFixture({ isActive: false });
      mockUserRepository.findByEmail.mockResolvedValue(user);

      await expect(
        authService.login({ email: user.email, password: 'password' })
      ).rejects.toThrow(UnauthorizedError);
    });
  });

  describe('register', () => {
    it('should register a new user successfully', async () => {
      const newUser = createUserEntityFixture();
      mockUserRepository.existsByEmail.mockResolvedValue(false);
      mockUserRepository.create.mockResolvedValue(newUser);

      const result = await authService.register({
        email: 'new@test.com',
        password: 'ValidP@ss123',
        name: 'Test User',
      });

      expect(result.user).toBeDefined();
      expect(result.tokens).toBeDefined();
    });

    it('should throw ConflictError if email exists', async () => {
      mockUserRepository.existsByEmail.mockResolvedValue(true);

      await expect(
        authService.register({
          email: 'existing@test.com',
          password: 'ValidP@ss123',
          name: 'Test User',
        })
      ).rejects.toThrow(ConflictError);
    });
  });
});
```

### Integration Tests

```typescript
// tests/integration/graphql/auth.graphql.test.ts
import request from 'supertest';
import { Application } from 'express';
import { createApp } from '../../../src/app';
import { resolvers } from '../../../src/container';
import { createUserEntityFixture } from '../../fixtures/user.fixture';
import { hashPassword } from '../../../src/utils/password';

// Note: Tests access db pool via DI container's resolvers for consistency

describe('Auth GraphQL Integration', () => {
  let app: Application;
  let db: ReturnType;

  beforeAll(async () => {
    app = createApp();
    db = resolvers.db();
  });

  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  afterAll(async () => {
    await db.end();
  });

  describe('Mutation: register', () => {
    const REGISTER_MUTATION = \`
      mutation Register(\$input: RegisterInput!) {
        register(input: \$input) {
          user {
            id
            email
            name
            role
          }
          tokens {
            accessToken
            refreshToken
          }
        }
      }
    \`;

    it('should register a new user', async () => {
      const response = await request(app)
        .post('/graphql')
        .send({
          query: REGISTER_MUTATION,
          variables: {
            input: {
              email: 'test@example.com',
              password: 'ValidP@ss123',
              name: 'Test User',
            },
          },
        });

      expect(response.status).toBe(200);
      expect(response.body.data.register.user.email).toBe('test@example.com');
      expect(response.body.data.register.tokens.accessToken).toBeDefined();
    });

    it('should return error for duplicate email', async () => {
      // Create existing user
      const hashedPassword = await hashPassword('password');
      await db.query(
        'INSERT INTO users (email, password, name) VALUES (\$1, \$2, \$3)',
        ['existing@example.com', hashedPassword, 'Existing']
      );

      const response = await request(app)
        .post('/graphql')
        .send({
          query: REGISTER_MUTATION,
          variables: {
            input: {
              email: 'existing@example.com',
              password: 'ValidP@ss123',
              name: 'Test User',
            },
          },
        });

      expect(response.body.errors).toBeDefined();
      expect(response.body.errors[0].code).toBe('CONFLICT');
    });
  });

  describe('Mutation: login', () => {
    const LOGIN_MUTATION = \`
      mutation Login(\$input: LoginInput!) {
        login(input: \$input) {
          user {
            id
            email
          }
          tokens {
            accessToken
            refreshToken
          }
        }
      }
    \`;

    it('should login with valid credentials', async () => {
      const hashedPassword = await hashPassword('ValidP@ss123');
      await db.query(
        'INSERT INTO users (email, password, name) VALUES (\$1, \$2, \$3)',
        ['login@example.com', hashedPassword, 'Login User']
      );

      const response = await request(app)
        .post('/graphql')
        .send({
          query: LOGIN_MUTATION,
          variables: {
            input: {
              email: 'login@example.com',
              password: 'ValidP@ss123',
            },
          },
        });

      expect(response.status).toBe(200);
      expect(response.body.data.login.user.email).toBe('login@example.com');
      expect(response.body.data.login.tokens.accessToken).toBeDefined();
    });
  });
});
```

### E2E Tests

```typescript
// tests/e2e/api.test.ts
import request from 'supertest';
import { Application } from 'express';
import { createApp } from '../../src/app';
import { resolvers } from '../../src/container';

// Note: Tests access db pool via DI container's resolvers for consistency

describe('API E2E Tests', () => {
  let app: Application;
  let accessToken: string;
  let db: ReturnType;

  beforeAll(async () => {
    app = createApp();
    db = resolvers.db();
  });

  beforeEach(async () => {
    await db.query('DELETE FROM users');
  });

  afterAll(async () => {
    await db.end();
  });

  describe('Health Endpoints', () => {
    it('GET /health should return healthy status', async () => {
      const response = await request(app).get('/health');

      expect(response.status).toBe(200);
      expect(response.body.status).toBe('healthy');
      expect(response.body.services.database).toBe('up');
    });

    it('GET /ready should return ready status', async () => {
      const response = await request(app).get('/ready');

      expect(response.status).toBe(200);
      expect(response.body.status).toBe('ready');
    });
  });

  describe('GraphQL Authentication Flow', () => {
    it('should complete full auth flow', async () => {
      // 1. Register
      const registerResponse = await request(app)
        .post('/graphql')
        .send({
          query: \`
            mutation {
              register(input: {
                email: "flow@example.com"
                password: "ValidP@ss123"
                name: "Flow User"
              }) {
                user { id email }
                tokens { accessToken refreshToken }
              }
            }
          \`,
        });

      expect(registerResponse.body.data.register.user.email).toBe('flow@example.com');
      accessToken = registerResponse.body.data.register.tokens.accessToken;
      const refreshToken = registerResponse.body.data.register.tokens.refreshToken;

      // 2. Get current user
      const meResponse = await request(app)
        .post('/graphql')
        .set('Authorization', \`Bearer \${accessToken}\`)
        .send({
          query: \`
            query {
              me {
                id
                email
                name
              }
            }
          \`,
        });

      expect(meResponse.body.data.me.email).toBe('flow@example.com');

      // 3. Refresh tokens
      const refreshResponse = await request(app)
        .post('/graphql')
        .send({
          query: \`
            mutation RefreshTokens(\$refreshToken: String!) {
              refreshTokens(refreshToken: \$refreshToken) {
                accessToken
                refreshToken
              }
            }
          \`,
          variables: { refreshToken },
        });

      expect(refreshResponse.body.data.refreshTokens.accessToken).toBeDefined();
    });
  });
});
```

## Package.json Scripts

```json
{
  "name": "typescript-express-graphql-backend",
  "version": "1.0.0",
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc",
    "start": "node dist/server.js",
    "lint": "eslint src --ext .ts",
    "lint:fix": "eslint src --ext .ts --fix",
    "format": "prettier --write \"src/**/*.ts\"",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:unit": "jest --testPathPattern=tests/unit",
    "test:integration": "jest --testPathPattern=tests/integration",
    "test:e2e": "jest --testPathPattern=tests/e2e",
    "docker:dev": "docker-compose -f docker/docker-compose.yml up --build",
    "docker:down": "docker-compose -f docker/docker-compose.yml down -v",
    "db:migrate": "psql -h localhost -U postgres -d appdb -f migrations/001_create_users_table.sql",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@apollo/server": "^4.10.0",
    "@escape.tech/graphql-armor": "^3.0.0",
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/auto-instrumentations-node": "^0.52.0",
    "@opentelemetry/sdk-node": "^0.52.0",
    "argon2": "^0.40.0",
    "compression": "^1.7.4",
    "cors": "^2.8.5",
    "dataloader": "^2.2.3",
    "express": "^4.18.2",
    "express-rate-limit": "^7.4.0",
    "graphql": "^16.8.1",
    "graphql-scalars": "^1.23.0",
    "helmet": "^7.1.0",
    "jose": "^5.9.0",
    "pg": "^8.11.3",
    "pino": "^8.17.2",
    "prom-client": "^15.1.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@faker-js/faker": "^8.3.1",
    "@testcontainers/postgresql": "^10.13.0",
    "@types/compression": "^1.7.5",
    "@types/cors": "^2.8.17",
    "@types/express": "^4.17.21",
    "@types/jest": "^29.5.11",
    "@types/node": "^20.10.6",
    "@types/pg": "^8.10.9",
    "@types/supertest": "^6.0.2",
    "jest": "^29.7.0",
    "jest-mock-extended": "^3.0.5",
    "pino-pretty": "^10.3.1",
    "supertest": "^6.3.3",
    "ts-jest": "^29.1.1",
    "tsx": "^4.7.0",
    "typescript": "^5.3.3"
  }
}
```

## Best Practices Checklist

### Security
- [ ] Use Helmet for security headers
- [ ] Implement proper CORS configuration
- [ ] Hash passwords with argon2 (PHC winner, memory-hard)
- [ ] Use short-lived JWTs with refresh tokens (jose library)
- [ ] Validate all inputs with Zod
- [ ] Sanitize error messages in production
- [ ] Use parameterized queries (prevent SQL injection)
- [ ] Implement rate limiting (express-rate-limit)
- [ ] Use HTTPS in production
- [ ] Limit GraphQL query depth/complexity (graphql-armor)

### Architecture
- [ ] Use dependency injection pattern (custom DI container)
- [ ] Use constructor injection for all dependencies
- [ ] Keep business logic in services
- [ ] Use repositories for data access
- [ ] Define clear type boundaries with TypeScript
- [ ] Use custom error classes for consistent error handling
- [ ] Implement DataLoader for N+1 query prevention

### Performance
- [ ] Use DataLoader to batch database queries
- [ ] Implement response caching where appropriate
- [ ] Use connection pooling (pg Pool)
- [ ] Enable compression middleware
- [ ] Limit pagination to prevent large queries

### Observability
- [ ] Use structured logging with Pino
- [ ] Implement OpenTelemetry tracing
- [ ] Expose Prometheus metrics (prom-client)
- [ ] Add correlation IDs to all requests
- [ ] Monitor query performance

### Code Quality
- [ ] Use TypeScript strict mode
- [ ] Implement comprehensive testing (unit, integration, E2E)
- [ ] Use Testcontainers for real database tests
- [ ] Maintain 80%+ test coverage
- [ ] Use ESLint and Prettier
- [ ] Document public APIs
- [ ] Use meaningful variable/function names

### DevOps
- [ ] Use multi-stage Docker builds
- [ ] Run as non-root user in containers
- [ ] Implement health check endpoints (/health, /ready)
- [ ] Configure graceful shutdown
- [ ] Set up proper environment configuration

### Database
- [ ] Use connection pooling
- [ ] Implement database transactions
- [ ] Create proper indexes
- [ ] Use migrations for schema changes
- [ ] Handle connection errors gracefully
- [ ] Consider Kysely for type-safe SQL

## Resources

- **Express.js Guide**: https://expressjs.com/en/guide/
- **Apollo Server**: https://www.apollographql.com/docs/apollo-server/
- **GraphQL Documentation**: https://graphql.org/learn/
- **DataLoader**: https://github.com/graphql/dataloader
- **graphql-armor**: https://escape.tech/graphql-armor/
- **Zod Documentation**: https://zod.dev/
- **Jest Documentation**: https://jestjs.io/docs/getting-started
- **Testcontainers**: https://testcontainers.com/guides/getting-started-with-testcontainers-for-nodejs/
- **PostgreSQL Documentation**: https://www.postgresql.org/docs/
- **Kysely**: https://kysely.dev/
- **argon2**: https://github.com/ranisalt/node-argon2
- **jose (JWT)**: https://github.com/panva/jose
- **OpenTelemetry**: https://opentelemetry.io/docs/languages/js/
- **Docker Best Practices**: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
- **Node.js Best Practices**: https://github.com/goldbergyoni/nodebestpractices

.claude/skills/fix-issue/SKILL.md

---
name: fix-issue
description: Fix a GitHub issue following our workflow
disable-model-invocation: true
allowed-tools: Read, Edit, Bash
---
Analyze and fix GitHub issue $ARGUMENTS:
1. Use `gh issue view $ARGUMENTS` to get issue details
2. Understand the problem and identify affected files
3. Write a failing test that reproduces the issue
4. Implement the fix
5. Ensure all tests pass: `npm run test`
6. Ensure linting passes: `npm run lint`
7. Create a descriptive commit: `git commit -m "fix: <description>"`
8. Push and create a PR: `gh pr create`

.claude/skills/add-resolver/SKILL.md

---
name: add-resolver
description: Add a new GraphQL resolver following project patterns
---
Create a new GraphQL resolver for \$ARGUMENTS:
1. Review existing resolvers in `src/graphql/resolvers/` for patterns
2. Create type definitions in `src/graphql/schema/`
3. Implement resolver in `src/graphql/resolvers/`
4. Add any required service methods
5. Create validation schemas in `src/validators/`
6. Write integration tests in `tests/integration/graphql/`
7. Run tests: `npm run test`

Invoking skills:

claude
> /fix-issue 123
> /add-resolver createOrganization

MCP Integration (Model Context Protocol)

Connect Claude Code to external tools:

# Connect to GitHub
claude mcp add --transport http github https://api.githubcopilot.com/mcp/
# Connect to PostgreSQL database
claude mcp add --transport stdio db -- npx -y @bytebase/dbhub \
  --dsn "postgresql://user:pass@localhost:5432/mydb"
# Connect to Sentry for error monitoring
claude mcp add --transport http sentry https://mcp.sentry.dev/mcp
# List configured servers
claude mcp list
# Check status within Claude Code
> /mcp

Using MCP in prompts:

claude
> Create a PR for issue #456 and assign it to the backend team
> What are the most common errors in the last 24 hours? (use Sentry)
> Show me the users table schema (use database MCP)

Context Management

Best Practices:

  1. Start fresh sessions for new tasks
    claude --session-id "auth-feature"
    # Later
    claude --resume auth-feature

  2. Use /clear between unrelated tasks
    > /clear

  3. Use /compact when context grows
    > /compact Focus on the GraphQL schema changes

  4. Delegate research to subagents
    > Use a subagent to research how our caching layer works

  5. Name and organize sessions
    > /rename auth-jwt-implementation

Context Degradation Signs

  • Claude «forgets» earlier instructions
  • Generated code quality declines
  • Claude asks questions you already answered
  • More errors and inconsistencies appear

Solution: Start fresh with a refined prompt incorporating what you learned.


CLI Reference and Essential Commands

Starting Claude Code

CommandDescription
claudeStart interactive session
claude "query"Start with initial prompt
claude -p "query"Headless mode (pipe output)
claude -cContinue most recent conversation
claude -r "name"Resume named session
claude --permission-mode planStart in Plan Mode

Interactive Commands

CommandDescription
/helpShow all commands
/clearReset context
/compact [focus]Summarize context
/rewindRestore previous checkpoint
/rename <name>Name current session
/resumeSession selector
/mcpMCP server status
/permissionsManage permissions
/skillsList available skills

Keyboard Shortcuts

ShortcutAction
Shift+TabCycle permission modes
EscStop Claude mid-action
Esc EscOpen rewind menu
Ctrl+GOpen plan in editor
Ctrl+OToggle verbose mode
Option+T (Mac)Toggle thinking mode

Useful Flags

# Skip permissions (use carefully!)
claude --dangerously-skip-permissions
# Specify model
claude --model claude-sonnet-4-5-20250929
# Output format for scripts
claude -p "query" --output-format json
# Add working directories
claude --add-dir ../shared-lib
# Custom system prompt
claude --append-system-prompt "Always use TypeScript strict mode"

Tips and Tricks: Expert-Level Practices

1. AskUserQuestion for Deep Planning

Use this tool when planning. It's criminally underutilized.

2. Don’t Obsess Over Tools

MCP, Skills, Plugins - they're fine, but won't fix a bad plan.
The SPECS is 90% of the work.

3. Build Without Ralph First

Gain experience. Understand the manual process before automating.
You need to know when Claude goes wrong to use Ralph effectively.

4. Context is King (50% Rule)

- Monitor context usage
- Clean up between unrelated tasks  
- Use subagents for research
- Start fresh when quality drops

5. Be Bold

Software development is becoming easy.
Software engineering (architecture, UX, taste) is still hard.
That's your competitive advantage.

6. Let Claude Interview You

> Interview me about this feature using AskUserQuestion.
> Ask about edge cases, error handling, and tradeoffs.
> Be annoying. I'd rather answer 20 questions than rework the code.

7. Verify Everything

Always give Claude a way to verify its work:
- Test commands
- Expected outputs  
- Screenshots for UI
- Lint checks

8. Course Correct Early

Esc → stops immediately
Esc Esc → rewind menu
"Undo that" → reverts changes
/clear → fresh start

Don't let bad code accumulate in the context.

Real-World Example: Express + GraphQL Backend

Let’s build a complete example step by step.

Step 1: Deep Planning Session

claude --permission-mode plan
I want to build a production-ready GraphQL API for a user management system.

Tech stack:
- TypeScript 5+
- Express.js  
- Apollo Server
- PostgreSQL with native pg client
- Custom DI container for dependency injection
- Zod validation
- Authentication: jose (JWT), argon2 (password hashing)
- GraphQL: DataLoader (N+1), graphql-armor (security)
- Security: Helmet, CORS, express-rate-limit
- Logging: Pino
- Observability: OpenTelemetry, prom-client
- Jest + Supertest + Testcontainers for testing

Interview me using AskUserQuestion about:
1. Database schema design
2. Authentication flow (access/refresh tokens)
3. Authorization model (roles/permissions)
4. Error handling strategy
5. API versioning
6. Rate limiting
7. Logging and monitoring (distributed tracing)
8. Testing strategy (testcontainers for real DB)
9. Deployment considerations

Be thorough. Ask about tradeoffs. Challenge my assumptions.
Write the final specification in PRD.md.

Step 2: Project Setup (Without Ralph)

# Exit plan mode, start normal session
claude
Let's start implementing our PRD.md. Begin with project setup:

1. Initialize npm project with TypeScript
2. Install dependencies from PRD.md tech stack
3. Configure tsconfig.json (strict mode)
4. Set up project structure as defined in PRD
5. Create basic Express server with health check
6. Add Jest configuration

After each step, verify with:
- npm run type-check
- npm run test

Show me the file structure when done.

Step 3: Feature-by-Feature Implementation

Database Layer:

Implement the database layer:
1. Create SQL migrations in migrations/ (users, sessions tables)
2. Create TypeScript models in src/models/ (User, Session)
3. Set up pg connection pool in src/config/database.ts
4. Create and run initial migration
5. Write a simple integration test that creates and reads a user

Run the test to verify everything works.

Repository Layer (DI Pattern):

Now implement UserRepository in src/repositories/user.repository.ts:
- findById, findByEmail, create, update, delete methods
- Receive database pool and logger via constructor injection
- Register as singleton in src/container/index.ts
- Write integration tests using resolvers.db() from DI container

Run tests after implementation.

Service Layer (DI Pattern):

Implement AuthService in src/services/auth.service.ts:
- Receive UserRepository, Config, and Logger via constructor injection
- register(email, password) - create user, hash password
- login(email, password) - verify credentials, issue tokens
- refreshToken(token) - validate and issue new tokens

Register AuthService as singleton in src/container/index.ts.
Follow our error handling patterns from PRD.md.
Write unit tests with mocked dependencies via jest-mock-extended.

GraphQL Layer (Services via DI Context):

Set up Apollo Server with our auth schema:
1. Define schema with SDL in src/graphql/schema/
2. User type and queries (me, user, users)
3. Auth mutations (register, login, refreshToken)
4. Create GraphQL context that resolves services from DI container
5. Resolvers access services via context.services (injected from container)
6. Apollo Server plugin for logging and error handling
7. Auth middleware for protected routes

Write integration tests for GraphQL endpoints using resolvers from DI container.

Step 4: Verification and Documentation

Before finishing:
1. Run full test suite: npm run test
2. Run linting: npm run lint
3. Type check: npm run type-check
4. Generate API documentation
5. Update README with setup instructions
6. Create Dockerfile for deployment

Report any issues.

Remember: Models are good enough now. If your output is garbage, your input was garbage. Invest in planning.


Official Documentation

Video Reference

Related Tools

MCP Servers


Reference


Checklist

Before starting any project with Claude Code:

  • Plan Deeply First: Use AskUserQuestion for thorough planning
  • Create PRD.md: Document tech stack, architecture, features, constraints
  • Set Up CLAUDE.md: Build commands, code style, testing instructions
  • Build Feature by Feature: Implement → Test → Verify → Commit
  • Monitor Context: Stay under 50%, clean up between tasks
  • Verify Everything: Tests, lint, type-check at every step
  • Document Progress: Keep progress.txt updated
  • Only Then Consider Ralph: After you understand the manual process

Claude Code Native Tools Reference

Claude Code has access to a set of powerful built-in tools that help it understand and modify your codebase. These are the official native tools as documented in the Claude Code settings reference:

Complete Native Tools Table

ToolDescriptionRequires Permission
AskUserQuestionAsks multiple-choice questions to gather requirements or clarify ambiguityNo
BashExecutes shell commands in your environmentYes
TaskOutputRetrieves output from a background task (bash shell or subagent)No
EditMakes targeted edits to specific filesYes
ExitPlanModePrompts the user to exit plan mode and start codingYes
GlobFinds files based on pattern matchingNo
GrepSearches for patterns in file contentsNo
KillShellKills a running background bash shell by its IDNo
MCPSearchSearches for and loads MCP tools when tool search is enabledNo
NotebookEditModifies Jupyter notebook cellsYes
ReadReads the contents of filesNo
SkillExecutes a skill within the main conversationYes
TaskRuns a sub-agent to handle complex, multi-step tasksNo
TaskCreateCreates a new task in the task listNo
TaskGetRetrieves full details for a specific taskNo
TaskListLists all tasks with their current statusNo
TaskUpdateUpdates task status, dependencies, details, or deletes tasksNo
WebFetchFetches content from a specified URLYes
WebSearchPerforms web searches with domain filteringYes
WriteCreates or overwrites filesYes
LSPCode intelligence via language servers. Reports type errors and warnings automatically after file edits. Also supports navigation operations: jump to definitions, find references, get type info, list symbols, find implementations, trace call hierarchies. Requires a code intelligence plugin and its language server binaryNo

Source: Official Claude Code documentation – Settings: Tools available to Claude

Permission Rule Syntax

Permission rules follow the format Tool or Tool(specifier):

# Match all uses of a tool
Bash                        # All bash commands
WebFetch                    # All web fetch requests
Read                        # All file reads

# Fine-grained control with specifiers
Bash(npm run build)         # Exact command
Read(./.env)                # Specific file
WebFetch(domain:example.com) # Domain restriction

# Wildcards with *
Bash(npm run *)             # All npm run commands
Bash(git commit *)          # Git commits
Bash(* --version)           # Version checks

Claude Code Hook Lifecycle Reference

Claude Code hooks are user-defined shell commands that execute at various points in Claude Code’s lifecycle. Hooks provide deterministic control over Claude Code’s behavior, ensuring certain actions always happen.

Official Hook Lifecycle Table

Hook EventTrigger Point
SessionStartSession begins or resumes
UserPromptSubmitUser submits a prompt
PreToolUseBefore tool execution
PermissionRequestWhen permission dialog appears
PostToolUseAfter tool succeeds
PostToolUseFailureAfter tool fails
SubagentStartWhen spawning a subagent
SubagentStopWhen subagent finishes
StopClaude finishes responding
PreCompactBefore context compaction
SessionEndSession terminates
NotificationClaude Code sends notifications
SetupInvoked with --init, --init-only, or --maintenance flags

Source: Official Claude Code documentation – Hooks Reference: Hook lifecycle

Hook Configuration Example

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' >> ~/.claude/bash-log.txt"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\$CLAUDE_PROJECT_DIR/.claude/hooks/auto-format.sh"
          }
        ]
      }
    ]
  }
}

Common Hook Use Cases

Use CaseHook EventExample
Logging commandsPreToolUseBashLog all bash commands executed
Auto-formattingPostToolUseEdit|WriteRun Prettier after file edits
Custom notificationsNotificationDesktop alerts for permission requests
File protectionPreToolUseEdit|WriteBlock edits to .env, secrets/
Context injectionSessionStartLoad environment variables, project context
CleanupSessionEndSave session stats, run cleanup scripts

Claude Code Sub-agents Reference

Sub-agents are specialized AI assistants that handle specific types of tasks. Each subagent runs in its own context window with a custom system prompt, specific tool access, and independent permissions.

Built-in Sub-agents

Sub-agentModelToolsPurpose
ExploreHaiku (fast)Read-only (denied Write, Edit)File discovery, code search, codebase exploration
PlanInheritRead-onlyCreate execution plans, analyze requirements
General-purposeInheritAll toolsGeneral task delegation

Source: Official Claude Code documentation – Sub-agents: Built-in subagents

Custom Sub-agent Configuration

Sub-agents are defined in Markdown files with YAML frontmatter:

---
name: code-reviewer
description: Expert code review specialist. Use proactively after code changes.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You are a senior code reviewer ensuring high standards of code quality and security.
When invoked:
1. Run git diff to see recent changes
2. Focus on modified files
3. Begin review immediately
Review checklist:
- Code is clear and readable
- Functions and variables are well-named
- Proper error handling
- No exposed secrets or API keys

Sub-agent Scope Locations

ScopeLocationPriorityUse Case
CLI flag--agents JSON1 (highest)Quick testing, automation
Project.claude/agents/2Project-specific, shared via git
User~/.claude/agents/3Personal, available across all projects
PluginPlugin’s agents/ directory4 (lowest)Distributed with plugins

Sub-agent Frontmatter Fields

FieldRequiredDescription
nameYesUnique identifier (lowercase, hyphens)
descriptionYesWhen Claude should delegate to this subagent
toolsNoTools the subagent can use (inherits all if omitted)
disallowedToolsNoTools to deny
modelNosonnet, opus, haiku, or inherit
permissionModeNodefault, acceptEdits, dontAsk, bypassPermissions, plan
skillsNoSkills to load into context at startup
hooksNoLifecycle hooks scoped to this subagent

Recommended Sub-agents for This Project

1. Code Reviewer Sub-agent

---
name: code-reviewer
description: Reviews TypeScript/GraphQL code for quality and best practices. Use after any code changes.
tools: Read, Grep, Glob, Bash
model: sonnet
---
You are a TypeScript + GraphQL code reviewer. Focus on:
- Type safety and proper TypeScript usage
- GraphQL resolver patterns and DataLoader usage
- DI container patterns (constructor injection, no decorators)
- Error handling with custom error classes
- Security (input validation, SQL injection prevention)
After each review, provide:
1. Critical issues (must fix)
2. Warnings (should fix)  
3. Suggestions (nice to have)

2. Test Writer Sub-agent

---
name: test-writer
description: Writes comprehensive tests for TypeScript code. Use when adding test coverage.
tools: Read, Write, Edit, Bash, Glob
model: sonnet
---
You are a testing expert for TypeScript + Jest + Supertest.
Test patterns to follow:
- Unit tests: Mock dependencies with jest-mock-extended
- Integration tests: Use Testcontainers for real PostgreSQL
- E2E tests: Full server bootstrap with Supertest
- Use Faker for realistic test data
Always structure tests as: Arrange → Act → Assert
Target 80%+ code coverage.

3. Debugger Sub-agent

---
name: debugger
description: Debugging specialist for errors, test failures, and unexpected behavior.
tools: Read, Edit, Bash, Grep, Glob
model: sonnet
---
You are an expert debugger for TypeScript + Node.js applications.
Debugging process:
1. Capture error message and stack trace
2. Identify reproduction steps
3. Isolate the failure location
4. Implement minimal fix
5. Verify solution works
For each issue, provide:
- Root cause explanation
- Evidence supporting the diagnosis
- Specific code fix
- Prevention recommendations

4. Database Query Expert

---
name: db-expert
description: PostgreSQL query expert. Use for database operations and optimizations.
tools: Bash, Read, Write
model: sonnet
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "\$CLAUDE_PROJECT_DIR/.claude/hooks/validate-sql.sh"
---
You are a PostgreSQL expert. Help with:
- Writing efficient SQL queries
- Creating migrations
- Query optimization
- Index recommendations
- Transaction handling
Always use parameterized queries to prevent SQL injection.

Based on the tech stack (TypeScript + Express + GraphQL + PostgreSQL), here are the most relevant MCP servers:

Development & Version Control

MCP ServerCommandUse Case
GitHubclaude mcp add --transport http github https://api.githubcopilot.com/mcp/PR reviews, issue management, code search
Linearclaude mcp add --transport http linear https://mcp.linear.app/mcpIssue tracking, project management

Database & Data

MCP ServerCommandUse Case
PostgreSQL (via Bytebase)claude mcp add --transport stdio db -- npx -y @bytebase/dbhub --dsn "postgresql://user:pass@host:5432/db"Natural language database queries

Monitoring & Debugging

MCP ServerCommandUse Case
Sentryclaude mcp add --transport http sentry https://mcp.sentry.dev/mcpError monitoring, production debugging

Documentation & Design

MCP ServerCommandUse Case
Notionclaude mcp add --transport http notion https://mcp.notion.com/mcpDocumentation, knowledge base
Figmaclaude mcp add --transport http figma https://mcp.figma.com/mcpDesign specs, UI/UX reference

CI/CD & Deployment

MCP ServerCommandUse Case
Netlifyclaude mcp add --transport http netlify https://mcp.netlify.app/v1/mcpDeployment management
Cloudflareclaude mcp add --transport http cloudflare https://bindings.mcp.cloudflare.com/mcpEdge functions, DNS

Project-Specific MCP Setup

Create .mcp.json in your project root for team-shared MCP configuration:

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/"
    },
    "sentry": {
      "type": "http",
      "url": "https://mcp.sentry.dev/mcp"
    },
    "database": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@bytebase/dbhub", "--dsn", "\${DATABASE_URL}"]
    }
  }
}

MCP Best Practices

  1. Use project scope for team-shared servers: claude mcp add --scope project ...
  2. Use user scope for personal tools: claude mcp add --scope user ...
  3. Authenticate via /mcp command for OAuth-based servers
  4. Set timeout for slow servers: MCP_TIMEOUT=10000 claude
  5. Monitor output limits: MAX_MCP_OUTPUT_TOKENS=50000 for large outputs

Source: Official Claude Code documentation – Connect Claude Code to tools via MCP

Happy Hacking!!!