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
71%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Complete guide to the layered architecture pattern used in backend microservices.
┌─────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────┘Testability:
Maintainability:
Reusability:
Scalability:
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 → ClientCritical: 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!
Strengths:
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.tsUse as template for new services!
Strengths:
Weaknesses:
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 patternLearn from: workflow/, middleware/auditMiddleware.ts Avoid: responseRoutes.ts, direct process.env
Purpose: Handle HTTP request/response concerns
Contents:
BaseController.ts - Base class with common methods{Feature}Controller.ts - Feature-specific controllersNaming: PascalCase + Controller
Responsibilities:
Purpose: Business logic and orchestration
Contents:
{feature}Service.ts - Feature business logicNaming: camelCase + Service (or PascalCase + Service)
Responsibilities:
Purpose: Data access abstraction
Contents:
{Entity}Repository.ts - Database operations for entityNaming: PascalCase + Repository
Responsibilities:
Current Gap: Only 1 repository exists (WorkflowRepository)
Purpose: Route registration ONLY
Contents:
{feature}Routes.ts - Express router for featureNaming: camelCase + Routes
Responsibilities:
Purpose: Cross-cutting concerns
Contents:
Naming: camelCase
Types:
Purpose: Configuration management
Contents:
unifiedConfig.ts - Type-safe configurationPattern: Single source of truth
Purpose: TypeScript type definitions
Contents:
{feature}.types.ts - Feature-specific typesFor large features, use subdirectories:
src/workflow/
├── core/ # Core engine
├── services/ # Workflow-specific services
├── actions/ # System actions
├── models/ # Domain models
├── validators/ # Workflow validation
└── utils/ # Workflow utilitiesWhen to use:
For simple features:
src/
├── controllers/UserController.ts
├── services/userService.ts
├── routes/userRoutes.ts
└── repositories/UserRepository.tsWhen to use:
Routes Layer:
Controllers Layer:
Services Layer:
Repositories Layer:
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: