CtrlK
BlogDocsLog inGet started
Tessl Logo

jbvc/backend-dev-guidelines

Opinionated backend development standards for Node.js + Express + TypeScript microservices. Covers layered architecture, BaseController pattern, dependency injection, Prisma repositories, Zod validation, unifiedConfig, Sentry error tracking, async safety, and testing discipline.

71

Quality

71%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

architecture-overview.mdresources/

Architecture Overview - Backend Services

Complete guide to the layered architecture pattern used in backend microservices.

Table of Contents

  • Layered Architecture Pattern
  • Request Lifecycle
  • Service Comparison
  • Directory Structure Rationale
  • Module Organization
  • Separation of Concerns

Layered Architecture Pattern

The Four Layers

┌─────────────────────────────────────┐
│         HTTP Request                │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  Layer 1: ROUTES                    │
│  - Route definitions only           │
│  - Middleware registration          │
│  - Delegate to controllers          │
│  - NO business logic                │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  Layer 2: CONTROLLERS               │
│  - Request/response handling        │
│  - Input validation                 │
│  - Call services                    │
│  - Format responses                 │
│  - Error handling                   │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  Layer 3: SERVICES                  │
│  - Business logic                   │
│  - Orchestration                    │
│  - Call repositories                │
│  - No HTTP knowledge                │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│  Layer 4: REPOSITORIES              │
│  - Data access abstraction          │
│  - Prisma operations                │
│  - Query optimization               │
│  - Caching                          │
└───────────────┬─────────────────────┘
                ↓
┌─────────────────────────────────────┐
│         Database (MySQL)            │
└─────────────────────────────────────┘

Why This Architecture?

Testability:

  • Each layer can be tested independently
  • Easy to mock dependencies
  • Clear test boundaries

Maintainability:

  • Changes isolated to specific layers
  • Business logic separate from HTTP concerns
  • Easy to locate bugs

Reusability:

  • Services can be used by routes, cron jobs, scripts
  • Repositories hide database implementation
  • Business logic not tied to HTTP

Scalability:

  • Easy to add new endpoints
  • Clear patterns to follow
  • Consistent structure

Request Lifecycle

Complete Flow Example

1. HTTP POST /api/users
   ↓
2. Express matches route in userRoutes.ts
   ↓
3. Middleware chain executes:
   - SSOMiddleware.verifyLoginStatus (authentication)
   - auditMiddleware (context tracking)
   ↓
4. Route handler delegates to controller:
   router.post('/users', (req, res) => userController.create(req, res))
   ↓
5. Controller validates and calls service:
   - Validate input with Zod
   - Call userService.create(data)
   - Handle success/error
   ↓
6. Service executes business logic:
   - Check business rules
   - Call userRepository.create(data)
   - Return result
   ↓
7. Repository performs database operation:
   - PrismaService.main.user.create({ data })
   - Handle database errors
   - Return created user
   ↓
8. Response flows back:
   Repository → Service → Controller → Express → Client

Middleware Execution Order

Critical: Middleware executes in registration order

app.use(Sentry.Handlers.requestHandler());  // 1. Sentry tracing (FIRST)
app.use(express.json());                     // 2. Body parsing
app.use(express.urlencoded({ extended: true })); // 3. URL encoding
app.use(cookieParser());                     // 4. Cookie parsing
app.use(SSOMiddleware.initialize());         // 5. Auth initialization
// ... routes registered here
app.use(auditMiddleware);                    // 6. Audit (if global)
app.use(errorBoundary);                      // 7. Error handler (LAST)
app.use(Sentry.Handlers.errorHandler());     // 8. Sentry errors (LAST)

Rule: Error handlers must be registered AFTER routes!


Service Comparison

Email Service (Mature Pattern ✅)

Strengths:

  • Comprehensive BaseController with Sentry integration
  • Clean route delegation (no business logic in routes)
  • Consistent dependency injection pattern
  • Good middleware organization
  • Type-safe throughout
  • Excellent error handling

Example Structure:

email/src/
├── controllers/
│   ├── BaseController.ts          ✅ Excellent template
│   ├── NotificationController.ts  ✅ Extends BaseController
│   └── EmailController.ts         ✅ Clean patterns
├── routes/
│   ├── notificationRoutes.ts      ✅ Clean delegation
│   └── emailRoutes.ts             ✅ No business logic
├── services/
│   ├── NotificationService.ts     ✅ Dependency injection
│   └── BatchingService.ts         ✅ Clear responsibility
└── middleware/
    ├── errorBoundary.ts           ✅ Comprehensive
    └── DevImpersonationSSOMiddleware.ts

Use as template for new services!

Form Service (Transitioning ⚠️)

Strengths:

  • Excellent workflow architecture (event sourcing)
  • Good Sentry integration
  • Innovative audit middleware (AsyncLocalStorage)
  • Comprehensive permission system

Weaknesses:

  • Some routes have 200+ lines of business logic
  • Inconsistent controller naming
  • Direct process.env usage (60+ occurrences)
  • Minimal repository pattern usage

Example:

form/src/
├── routes/
│   ├── responseRoutes.ts          ❌ Business logic in routes
│   └── proxyRoutes.ts             ✅ Good validation pattern
├── controllers/
│   ├── formController.ts          ⚠️ Lowercase naming
│   └── UserProfileController.ts   ✅ PascalCase naming
├── workflow/                      ✅ Excellent architecture!
│   ├── core/
│   │   ├── WorkflowEngineV3.ts   ✅ Event sourcing
│   │   └── DryRunWrapper.ts      ✅ Innovative
│   └── services/
└── middleware/
    └── auditMiddleware.ts         ✅ AsyncLocalStorage pattern

Learn from: workflow/, middleware/auditMiddleware.ts Avoid: responseRoutes.ts, direct process.env


Directory Structure Rationale

Controllers Directory

Purpose: Handle HTTP request/response concerns

Contents:

  • BaseController.ts - Base class with common methods
  • {Feature}Controller.ts - Feature-specific controllers

Naming: PascalCase + Controller

Responsibilities:

  • Parse request parameters
  • Validate input (Zod)
  • Call appropriate service methods
  • Format responses
  • Handle errors (via BaseController)
  • Set HTTP status codes

Services Directory

Purpose: Business logic and orchestration

Contents:

  • {feature}Service.ts - Feature business logic

Naming: camelCase + Service (or PascalCase + Service)

Responsibilities:

  • Implement business rules
  • Orchestrate multiple repositories
  • Transaction management
  • Business validations
  • No HTTP knowledge (Request/Response types)

Repositories Directory

Purpose: Data access abstraction

Contents:

  • {Entity}Repository.ts - Database operations for entity

Naming: PascalCase + Repository

Responsibilities:

  • Prisma query operations
  • Query optimization
  • Database error handling
  • Caching layer
  • Hide Prisma implementation details

Current Gap: Only 1 repository exists (WorkflowRepository)

Routes Directory

Purpose: Route registration ONLY

Contents:

  • {feature}Routes.ts - Express router for feature

Naming: camelCase + Routes

Responsibilities:

  • Register routes with Express
  • Apply middleware
  • Delegate to controllers
  • NO business logic!

Middleware Directory

Purpose: Cross-cutting concerns

Contents:

  • Authentication middleware
  • Audit middleware
  • Error boundaries
  • Validation middleware
  • Custom middleware

Naming: camelCase

Types:

  • Request processing (before handler)
  • Response processing (after handler)
  • Error handling (error boundary)

Config Directory

Purpose: Configuration management

Contents:

  • unifiedConfig.ts - Type-safe configuration
  • Environment-specific configs

Pattern: Single source of truth

Types Directory

Purpose: TypeScript type definitions

Contents:

  • {feature}.types.ts - Feature-specific types
  • DTOs (Data Transfer Objects)
  • Request/Response types
  • Domain models

Module Organization

Feature-Based Organization

For large features, use subdirectories:

src/workflow/
├── core/              # Core engine
├── services/          # Workflow-specific services
├── actions/           # System actions
├── models/            # Domain models
├── validators/        # Workflow validation
└── utils/             # Workflow utilities

When to use:

  • Feature has 5+ files
  • Clear sub-domains exist
  • Logical grouping improves clarity

Flat Organization

For simple features:

src/
├── controllers/UserController.ts
├── services/userService.ts
├── routes/userRoutes.ts
└── repositories/UserRepository.ts

When to use:

  • Simple features (< 5 files)
  • No clear sub-domains
  • Flat structure is clearer

Separation of Concerns

What Goes Where

Routes Layer:

  • ✅ Route definitions
  • ✅ Middleware registration
  • ✅ Controller delegation
  • ❌ Business logic
  • ❌ Database operations
  • ❌ Validation logic (should be in validator or controller)

Controllers Layer:

  • ✅ Request parsing (params, body, query)
  • ✅ Input validation (Zod)
  • ✅ Service calls
  • ✅ Response formatting
  • ✅ Error handling
  • ❌ Business logic
  • ❌ Database operations

Services Layer:

  • ✅ Business logic
  • ✅ Business rules enforcement
  • ✅ Orchestration (multiple repos)
  • ✅ Transaction management
  • ❌ HTTP concerns (Request/Response)
  • ❌ Direct Prisma calls (use repositories)

Repositories Layer:

  • ✅ Prisma operations
  • ✅ Query construction
  • ✅ Database error handling
  • ✅ Caching
  • ❌ Business logic
  • ❌ HTTP concerns

Example: User Creation

Route:

router.post('/users',
    SSOMiddleware.verifyLoginStatus,
    auditMiddleware,
    (req, res) => userController.create(req, res)
);

Controller:

async create(req: Request, res: Response): Promise<void> {
    try {
        const validated = createUserSchema.parse(req.body);
        const user = await this.userService.create(validated);
        this.handleSuccess(res, user, 'User created');
    } catch (error) {
        this.handleError(error, res, 'create');
    }
}

Service:

async create(data: CreateUserDTO): Promise<User> {
    // Business rule: check if email already exists
    const existing = await this.userRepository.findByEmail(data.email);
    if (existing) throw new ConflictError('Email already exists');

    // Create user
    return await this.userRepository.create(data);
}

Repository:

async create(data: CreateUserDTO): Promise<User> {
    return PrismaService.main.user.create({ data });
}

async findByEmail(email: string): Promise<User | null> {
    return PrismaService.main.user.findUnique({ where: { email } });
}

Notice: Each layer has clear, distinct responsibilities!


Related Files:

  • SKILL.md - Main guide
  • routing-and-controllers.md - Routes and controllers details
  • services-and-repositories.md - Service and repository patterns

resources

architecture-overview.md

async-and-errors.md

complete-examples.md

configuration.md

database-patterns.md

middleware-guide.md

routing-and-controllers.md

sentry-and-monitoring.md

services-and-repositories.md

testing-guide.md

validation-patterns.md

SKILL.md

tile.json