Claude Code Plugins

Community-maintained marketplace

Feedback

Expert in Lighthouse Journey Timeline backend architecture, service patterns, testing, and API development using Express.js, TypeScript, and Drizzle ORM. Use when implementing backend services, APIs, controllers, database queries, authentication, permissions, error handling, testing server code, or writing migrations.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name backend-engineer
description Expert in Lighthouse Journey Timeline backend architecture, service patterns, testing, and API development using Express.js, TypeScript, and Drizzle ORM. Use when implementing backend services, APIs, controllers, database queries, authentication, permissions, error handling, testing server code, or writing migrations.

Lighthouse Backend Engineer

Expert knowledge of backend patterns and architecture for the Lighthouse Journey Timeline.

๐Ÿ—๏ธ Core Architecture

Technology Stack

  • Runtime: Node.js with TypeScript
  • Framework: Express.js
  • Database: PostgreSQL with Drizzle ORM
  • DI Container: Awilix (Proxy injection mode)
  • Testing: Vitest with vitest-mock-extended
  • Validation: Zod schemas (CRITICAL: Use across ALL layers)
  • Documentation: express-jsdoc-swagger โ†’ OpenAPI

Service Layer Architecture

HTTP Request โ†’ Routes โ†’ Middleware โ†’ Controller โ†’ Service โ†’ Repository โ†’ Database
                                          โ†“           โ†“           โ†“
                                     Validation  Business    Drizzle
                                     & Mapping     Logic        ORM

๐Ÿ“ CRITICAL: Where to Look Before Making Changes

Pattern References (ALWAYS CHECK THESE FIRST)

Pattern Primary Reference Example Implementation
New Service src/services/hierarchy-service.ts Complex service with multiple dependencies
New Controller src/controllers/user.controller.ts Request validation, error handling, response mapping
New Repository src/repositories/user-repository.ts Drizzle patterns, complex queries
New Route src/routes/hierarchy.routes.ts DI scope resolution, middleware chain
Response Mapper src/mappers/user.mapper.ts DTO transformation patterns
Unit Tests src/services/__tests__/*.test.ts Mock setup, test structure
Error Codes src/core/error-codes.ts 70+ standardized error codes
API Documentation openapi-schema.yaml Current API specs - MUST UPDATE

โšก CRITICAL: Zod Validation Across All Layers

Why Zod Everywhere?

Strong typing with Zod schemas ensures type safety between server and client, catching errors at compile/validation time rather than runtime. EVERY data boundary must have Zod validation.

๐ŸŽฏ CRITICAL: Use Enums and Constants, Not Magic Strings

Never use magic strings. Always use enums or constants for:

  • Error codes: src/core/error-codes.ts - STRING enums (not numbers)
  • Node types: z.enum(['job', 'education', 'project', 'event'])
  • Permission levels: Define as const objects or Zod enums
  • Status values: Use Zod enums for type safety
  • Configuration: Store in constants files, not inline strings

Example - Error Codes:

// โœ… GOOD: Using enum
export enum ErrorCode {
  USER_NOT_FOUND = 'USER_NOT_FOUND',
  INVALID_PERMISSION = 'INVALID_PERMISSION',
}
throw new AppError(ErrorCode.USER_NOT_FOUND);

// โŒ BAD: Magic strings
throw new AppError('USER_NOT_FOUND');

Example - Zod Enums:

// โœ… GOOD: Zod enum for validation + TypeScript type
const NodeTypeSchema = z.enum(['job', 'education', 'project']);
type NodeType = z.infer<typeof NodeTypeSchema>;

// โŒ BAD: Plain strings
type NodeType = string;

Layer-by-Layer Validation

1. Controller Layer (Request Validation)

// Define request/response schemas in controller or separate file
const CreateNodeRequestSchema = z.object({
  type: z.enum(['job', 'education', 'project', 'event']),
  parentId: z.string().uuid().optional(),
  meta: z.object({
    title: z.string().min(1).max(200),
    company: z.string().optional(),
    startDate: z.string().datetime(),
    endDate: z.string().datetime().optional(),
    description: z.string().optional()
  })
});

// In controller method
async createNode(req: Request, res: Response) {
  // Validate request body
  const validatedData = CreateNodeRequestSchema.parse(req.body);
  // validatedData is now strongly typed

  // Pass to service (type-safe)
  const result = await this.nodeService.create(validatedData);
}

2. Service Layer (Business Logic Validation)

// Service method input schemas
const ServiceCreateNodeSchema = z.object({
  userId: z.number().positive(),
  type: z.enum(['job', 'education', 'project']),
  parentId: z.string().uuid().optional(),
  meta: z.record(z.any())
});

// Service method
async create(input: z.infer<typeof ServiceCreateNodeSchema>) {
  // Additional business validation
  const validated = ServiceCreateNodeSchema.parse(input);

  // Business rules validation
  if (validated.parentId) {
    await this.validateParentAccess(validated.parentId, validated.userId);
  }

  return await this.repository.create(validated);
}

3. Repository Layer (Database Schema Validation)

// Database insert/update schemas
const DbNodeInsertSchema = z.object({
  id: z.string().uuid().default(() => crypto.randomUUID()),
  type: z.string(),
  userId: z.number(),
  parentId: z.string().uuid().nullable(),
  meta: z.any(), // JSON column
  createdAt: z.date().default(() => new Date()),
  updatedAt: z.date().default(() => new Date())
});

// Repository method
async create(data: unknown) {
  // Validate before database operation
  const dbData = DbNodeInsertSchema.parse(data);
  return await this.db.insert(nodes).values(dbData);
}

4. Response Validation (DTO Layer)

// Response DTOs with Zod
const NodeResponseSchema = z.object({
  id: z.string().uuid(),
  type: z.string(),
  meta: z.object({
    title: z.string(),
    // ... other fields
  }),
  createdAt: z.string().datetime(),
  permissions: z.object({
    canView: z.boolean(),
    canEdit: z.boolean(),
    canDelete: z.boolean()
  })
});

// In mapper
static toDto(node: unknown): NodeResponseDto {
  // Validate and transform
  return NodeResponseSchema.parse({
    id: node.id,
    type: node.type,
    // ... mapping
  });
}

Shared Schema Patterns

Location Strategy

packages/
โ”œโ”€โ”€ schema/
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ”œโ”€โ”€ api/                        # โญ API Contracts (PRIMARY - ALWAYS USE)
โ”‚       โ”‚   โ”œโ”€โ”€ auth.schemas.ts         # Auth request/response schemas
โ”‚       โ”‚   โ”œโ”€โ”€ user.schemas.ts         # User request/response schemas
โ”‚       โ”‚   โ”œโ”€โ”€ timeline.schemas.ts     # Timeline request/response schemas
โ”‚       โ”‚   โ”œโ”€โ”€ files.schemas.ts        # File upload schemas
โ”‚       โ”‚   โ”œโ”€โ”€ common.schemas.ts       # Shared API schemas (pagination, etc.)
โ”‚       โ”‚   โ”œโ”€โ”€ validation-helpers.ts   # Reusable validators
โ”‚       โ”‚   โ””โ”€โ”€ index.ts                # Re-exports all API schemas
โ”‚       โ”œโ”€โ”€ schema.ts                   # Drizzle database schema
โ”‚       โ””โ”€โ”€ types.ts                    # Database type exports
โ””โ”€โ”€ server/
    โ””โ”€โ”€ src/
        โ”œโ”€โ”€ controllers/                # Import from @journey/schema/src/api
        โ”œโ”€โ”€ services/                   # Import from @journey/schema/src/api
        โ””โ”€โ”€ mappers/                    # Transform DB types โ†’ API response types

CRITICAL:

  • All API request/response contracts MUST be defined in @journey/schema/src/api/
  • Never create validation schemas in server/src/ - always use @journey/schema/src/api/
  • Controllers import and use these schemas directly
  • This ensures type safety between frontend and backend

Reusable Schema Components

// packages/schema/src/api/common.schemas.ts (or validation-helpers.ts)
export const uuidSchema = z.string().uuid();
export const dateTimeSchema = z.string().datetime();
export const paginationSchema = z.object({
  page: z.number().positive().default(1),
  limit: z.number().positive().max(100).default(20),
});

// Compose in controller-specific schemas
// packages/schema/src/api/timeline.schemas.ts
import { uuidSchema, paginationSchema } from './common.schemas';

export const getNodesRequestSchema = z.object({
  userId: uuidSchema,
  ...paginationSchema.shape,
});

export type GetNodesRequest = z.infer<typeof getNodesRequestSchema>;

Zod with Drizzle ORM

// Generate Zod schemas from Drizzle tables
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { users } from '@journey/schema';

// Auto-generate schemas from Drizzle table definitions
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);

// Extend with custom validations
export const createUserSchema = insertUserSchema
  .extend({
    email: z.string().email(),
    password: z.string().min(8).max(100),
  })
  .omit({ id: true, createdAt: true });

Error Handling for Zod Validation

// In controller error handling
import { ZodError } from 'zod';
import { fromZodError } from 'zod-validation-error';

try {
  const validated = schema.parse(req.body);
} catch (error) {
  if (error instanceof ZodError) {
    // Convert to friendly error message
    const validationError = fromZodError(error);
    return res.status(400).json({
      success: false,
      error: {
        code: 'VALIDATION_ERROR',
        message: validationError.toString(),
        details: error.issues,
      },
    });
  }
  throw error;
}

Type Inference Pattern

// Define schema once
const NodeCreateSchema = z.object({
  type: z.enum(['job', 'education']),
  meta: z.object({
    title: z.string(),
    company: z.string().optional(),
  }),
});

// Infer TypeScript type from schema
type NodeCreateInput = z.infer<typeof NodeCreateSchema>;

// Use throughout application
class NodeService {
  async create(input: NodeCreateInput) {
    // input is strongly typed
  }
}

Testing Zod Schemas

describe('NodeCreateSchema', () => {
  it('should validate correct input', () => {
    const input = {
      type: 'job',
      meta: { title: 'Software Engineer' },
    };

    expect(() => NodeCreateSchema.parse(input)).not.toThrow();
  });

  it('should reject invalid type', () => {
    const input = {
      type: 'invalid',
      meta: { title: 'Test' },
    };

    expect(() => NodeCreateSchema.parse(input)).toThrow(ZodError);
  });
});

Best Practices

  1. Parse, Don't Validate: Use parse() for runtime validation, safeParse() when you need to handle errors
  2. Single Source of Truth: Define schemas once in @journey/schema package
  3. Compose Schemas: Build complex schemas from simple, reusable parts
  4. Type Inference: Use z.infer<> instead of manually defining types
  5. Validate Early: Validate at system boundaries (API endpoints, external services)
  6. Document Schemas: Add descriptions for better OpenAPI generation
    z.string().describe('User email address');
    

๐Ÿ” Authentication Patterns

JWT Implementation

  • Access Token: 15min expiry, contains userId and basic claims
  • Refresh Token: 7 days, stored in DB, rotated on use
  • Token Service: src/services/jwt.service.ts
  • Refresh Service: src/services/refresh-token.service.ts

Auth Middleware Stack

// From src/middleware/auth.middleware.ts
requireAuth; // Validates JWT, loads user, fails if invalid
optionalAuth; // Validates JWT if present, continues regardless
requireGuest; // Ensures user is NOT authenticated
requireRole(role); // Role-based access control
requirePermission(); // Permission-based access

Token Refresh Flow

  1. Client sends refresh token to /api/auth/refresh
  2. Validate refresh token exists in DB and not expired
  3. Rotate: Mark old token as used, create new token pair
  4. Return new access + refresh tokens
  5. Clean up expired tokens periodically

๐Ÿงช Unit Testing Patterns

Test Structure (TDD Approach)

// Standard test file structure
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mock, mockDeep } from 'vitest-mock-extended';

describe('ServiceName', () => {
  let service: ServiceName;
  let mockRepo: MockType<Repository>;

  beforeEach(() => {
    vi.clearAllMocks();
    mockRepo = mock<Repository>();
    service = new ServiceName(mockRepo);
  });

  describe('methodName', () => {
    it('should handle success case', async () => {
      // Arrange
      const input = {
        /* test data */
      };
      const expected = {
        /* expected result */
      };
      mockRepo.findById.mockResolvedValue(expected);

      // Act
      const result = await service.method(input);

      // Assert
      expect(result).toEqual(expected);
      expect(mockRepo.findById).toHaveBeenCalledWith(input.id);
    });

    it('should handle error case', async () => {
      // Test error scenarios
    });
  });
});

Common Mock Patterns

// Mock with specific return
mockService.method.mockResolvedValue(result);

// Mock with implementation
mockService.method.mockImplementation(async (id) => {
  return id === 'valid' ? data : null;
});

// Mock chainable queries (Drizzle)
const mockQuery = {
  select: vi.fn().mockReturnThis(),
  from: vi.fn().mockReturnThis(),
  where: vi.fn().mockReturnThis(),
  limit: vi.fn().mockResolvedValue([result]),
};

// Reset mocks
mockReset(mockService); // Clear all mock data
mockClear(mockService); // Clear call history only

๐Ÿ—„๏ธ Database & Schema

Schema Management

  • Schema Location: packages/schema/src/schema.ts
  • Migrations: packages/schema/migrations/
  • Commands:
    cd packages/schema
    pnpm db:generate --name migration_name  # Generate migration
    pnpm db:migrate                         # Run migrations
    pnpm db:studio                          # Open Drizzle Studio
    

Drizzle ORM Patterns

// Repository pattern example
class UserRepository {
  constructor(private db: NodePgDatabase) {}

  async findById(id: number) {
    return this.db.query.users.findFirst({
      where: eq(users.id, id),
    });
  }

  async search(term: string) {
    return this.db
      .select()
      .from(users)
      .where(
        or(
          ilike(users.firstName, `%${term}%`),
          ilike(users.lastName, `%${term}%`)
        )
      )
      .limit(20);
  }
}

๐Ÿ”’ Permission Cascade Pattern

Hierarchy

Node Level (most specific)
    โ†“
Organization Level (if node belongs to org member)
    โ†“
User Level (node owner)
    โ†“
Public Level (if explicitly set)

Permission Check Flow

  1. Check Node Policies (node_policies table)

    • Direct user permissions
    • Organization permissions (if user is org member)
    • Public permissions
  2. Check Ownership

    • Node owner has full access
    • Parent node owners have view access
  3. Apply Most Permissive

    • If multiple policies exist, most permissive wins
    • DENY effects override ALLOW

Implementation Reference

  • Service: src/services/node-permission.service.ts
  • Repository: src/repositories/node-permission.repository.ts
  • SQL CTE: src/repositories/sql/permission-cte.ts

โŒ Error Handling

Error Code Reference

// Location: src/core/error-codes.ts
// NOTE: Error codes are STRING enums, not numbers
export enum ErrorCode {
  // Auth errors
  AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED',
  INVALID_TOKEN = 'INVALID_TOKEN',
  TOKEN_EXPIRED = 'TOKEN_EXPIRED',

  // Validation errors
  VALIDATION_ERROR = 'VALIDATION_ERROR',
  INVALID_REQUEST = 'INVALID_REQUEST',

  // Resource errors
  NOT_FOUND = 'NOT_FOUND',
  ALREADY_EXISTS = 'ALREADY_EXISTS',

  // Business errors
  BUSINESS_RULE_VIOLATION = 'BUSINESS_RULE_VIOLATION',
  CIRCULAR_REFERENCE = 'CIRCULAR_REFERENCE',
  MAX_DEPTH_EXCEEDED = 'MAX_DEPTH_EXCEEDED',

  // Server errors
  INTERNAL_ERROR = 'INTERNAL_ERROR',
  DATABASE_ERROR = 'DATABASE_ERROR',
  EXTERNAL_SERVICE_ERROR = 'EXTERNAL_SERVICE_ERROR',
}

// Check error-codes.ts for complete list (70+ codes)

Error Usage Pattern

// In service layer
throw new ValidationError(ErrorCode.VALIDATION_ERROR, 'Detailed error message');

// In controller - automatically handled by error middleware
// Returns standardized ApiErrorResponse

๐Ÿ“ API Documentation

JSDoc Requirements

/**
 * @route POST /api/v2/timeline/nodes
 * @summary Create timeline node
 * @description Creates a new node in user's timeline hierarchy
 * @body {CreateNodeDto} Node creation data
 * @response {201} {ApiSuccessResponse<Node>} Node created
 * @response {400} {ApiErrorResponse} Validation error
 * @response {403} {ApiErrorResponse} Permission denied
 * @security BearerAuth
 */
async createNode(req: Request, res: Response) {
  // Implementation
}

OpenAPI Verification

  • Schema File: packages/server/openapi-schema.yaml
  • Generation: Automatic from JSDoc comments
  • Verification: After API changes, verify schema is updated:
    pnpm generate:swagger
    # Check git diff on openapi-schema.yaml
    

๐Ÿ“œ One-Time Scripts Pattern

Script Structure

#!/usr/bin/env tsx

/**
 * Script Name and Purpose
 *
 * Run with: NODE_ENV=development tsx scripts/script-name.ts
 */

import { Container } from '../src/core/container-setup';
import { CONTAINER_TOKENS } from '../src/core/container-tokens';

async function main() {
  console.log('๐Ÿš€ Starting script...');

  // Initialize container (reuse app services)
  await Container.configure(console);
  const container = Container.getRootContainer();

  // Resolve services
  const userService = container.resolve(CONTAINER_TOKENS.USER_SERVICE);
  const hierarchyService = container.resolve(
    CONTAINER_TOKENS.HIERARCHY_SERVICE
  );

  try {
    // Use services exactly like controllers do
    const users = await userService.getAllUsers();

    // Process data...

    console.log('โœ… Script completed successfully');
  } catch (error) {
    console.error('โŒ Script failed:', error);
    process.exit(1);
  }
}

// Run if executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
  main().catch(console.error);
}

Script Locations

  • Database Scripts: packages/server/scripts/
  • Migration Scripts: packages/schema/migrations/
  • Package Commands: See package.json scripts section

๐Ÿ”„ Response Mapping Patterns

Mapper Location Strategy

// Check for existing mapper
src / mappers / [entity].mapper.ts;

// If none exists, check similar entities
src / mappers / user.mapper.ts; // User-related mappings
src / mappers / experience.mapper.ts; // Experience-related

// Mapper pattern
class EntityMapper {
  static toDto(entity: Entity): EntityDto {
    return {
      id: entity.id,
      // Transform fields
      // Omit internal fields
      // Format dates
    };
  }

  static toDtoArray(entities: Entity[]): EntityDto[] {
    return entities.map((e) => this.toDto(e));
  }
}

Response Wrapper

// All responses use standardized format
interface ApiSuccessResponse<T> {
  success: true;
  data: T;
}

// In controller
return res.status(200).json({
  success: true,
  data: UserMapper.toDto(user),
});

๐ŸŽฏ Development Workflow

Before Making Changes

  1. Check Pattern References (table above)
  2. Read Existing Implementation of similar feature
  3. Check Error Codes for appropriate errors
  4. Write Tests First (TDD approach)
  5. Update OpenAPI documentation via JSDoc

Common Commands

# From packages/server
pnpm test:unit              # Run unit tests only (fast)
pnpm test                   # All tests including integration
pnpm dev                    # Start dev server

# Run specific test file (fastest for TDD)
pnpm vitest run --no-coverage src/services/__tests__/user.service.test.ts

# From project root (Nx commands)
pnpm test:changed           # Test only changed packages
pnpm test:changed:all       # Include e2e for changed packages

๐Ÿ“š Learning & Skill Updates

How to Update This Skill

  1. Identify New Pattern: During implementation, notice repeated pattern
  2. Verify Pattern: Ensure it's used in multiple places
  3. Document Location: Add to "Pattern References" table
  4. Add Example: Include minimal code example if complex
  5. Update Skill: Edit this file at .claude/skills/backend-engineer/SKILL.md

What to Document

  • โœ… New service patterns that differ from existing
  • โœ… Complex query patterns (CTEs, joins)
  • โœ… New middleware patterns
  • โœ… Integration patterns with external services
  • โœ… Performance optimization patterns
  • โŒ One-off implementations
  • โŒ Temporary workarounds
  • โŒ Feature-specific business logic

Memory Integration

  • Project Patterns: Document in Serena memory via mcp__serena__write_memory
  • Reusable Patterns: Save to Memory MCP via mcp__memory__create_entities
  • Skill Updates: Direct edits to this file

๐Ÿš€ Quick Reference

DI Container Tokens

// Infrastructure
CONTAINER_TOKENS.DATABASE;
CONTAINER_TOKENS.LOGGER;

// Services (check container-tokens.ts for complete list)
CONTAINER_TOKENS.USER_SERVICE;
CONTAINER_TOKENS.HIERARCHY_SERVICE;
CONTAINER_TOKENS.NODE_PERMISSION_SERVICE;
// ... 15+ more services

// Controllers
CONTAINER_TOKENS.USER_CONTROLLER;
CONTAINER_TOKENS.HIERARCHY_CONTROLLER;
// ... 9+ controllers

Test Coverage Requirements

  • Target: 80% coverage minimum
  • Critical Paths: 100% coverage for auth, permissions
  • Check Coverage: pnpm test:coverage
  • View Report: pnpm coverage:html

Performance Considerations

  • Query Optimization: Use Drizzle's query builder over raw SQL
  • Pagination: Always paginate lists (default 20, max 100)
  • N+1 Prevention: Use joins or batch queries
  • Caching: Consider Redis for frequently accessed data

Remember: Always check existing patterns before implementing new ones. This maintains consistency and reduces technical debt.