Controllers
This document outlines the controller patterns and architecture used across the API.
Overview
The API follows a consistent controller pattern across all features, with 22 controllers organized by feature domains:
-
Auth (2 controllers): login, register
-
Decisions (1 controller): select-gift
-
Gifts (2 controllers): get-gift, get-gifts
-
Notifications (2 controllers): get-notifications, send-notification
-
Occasions (7 controllers): create, delete, get, get-all, add-participant, get-participants, update
-
Options (1 controller): get-options
-
Relationships (2 controllers): add-relationship, get-relationships
-
Users (5 controllers): create, delete, get, get-all, update
Controller Architecture
Core Pattern
All controllers follow this consistent structure:
import { Request, Response } from "express"; import { asyncHandler } from "@/lib/express/express.async-handler"; import { sendSuccessResponse, sendErrorResponse } from "@/lib/express/express.response"; import { HttpStatus } from "@/lib/http/http.status"; import { SerializedEntity } from "../feature.config"; import { operationFunction } from "../operations/feature.operation";
export const controllerName = asyncHandler( async (req: Request, res: Response) => { // 1. Extract validated request data (validated by middleware) const validatedData = req.validated.body; // Pre-validated, type-safe const id = req.params.id as string;
// 2. Execute business logic (no validation needed) const result = await operationFunction(validatedData, id);
// 3. Send response sendSuccessResponse( req, res, { data: result, serializerConfig: SerializedEntity, type: "single", // or "collection" }, { status: HttpStatus.SUCCESS, // or other status }, );
// 4. Optional: Emit events/queue operations await featureQueue.enqueue({ event: FeatureEventTypes.EventName, payload: result, metadata: { source: "controller-name" }, });
}, );
Route Configuration with Validation:
// Validation happens before controller execution
router.post("/", validateHttpRequest(createSchema, "body"), controllerName);
Key Components
1. Async Handler Wrapper
export const controllerName = asyncHandler(
async (req: Request, res: Response) => {
// Controller logic
},
);
The asyncHandler automatically catches errors and forwards them to Express error handling middleware.
2. Request Data Extraction
// Body data
const { name, email } = req.body ?? {};
// Route parameters
const id = req.params.id as string;
const { processId, giftId } = req.params;
// Query parameters (if needed)
const page = req.query.page as string;
3. Input Validation (Handled by Middleware)
Controllers do not handle input validation directly. Validation is performed by middleware before the request reaches the controller. This separation of concerns ensures clean, focused controllers.
Validation Middleware Pattern:
// routes file occasionsRouter.post("/", validateHttpRequest(occasionInsertSchema, "body"), createOccasionController );
Validation Process:
// middleware/http-request-validator.ts
export function validateHttpRequest<T extends z.ZodTypeAny>(
schema: T,
location: RequestLocation, // "body" | "params" | "query" | "headers"
) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req[location]);
if (!result.success) {
// Automatically sends JSON:API error response
const errors = result.error.issues.map((issue) => ({
status: "400",
code: issue.code,
title: "Validation Error",
detail: issue.message,
source: {
pointer: </span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>location<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>issue<span class="token punctuation">.</span>path<span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">,
},
}));
return sendErrorResponse(req, res, errors);
}
// Store validated data for controller to use req.validated[location] = result.data; next();
}; }
Controller Access to Validated Data:
// Controllers receive pre-validated data export const createOccasionController = asyncHandler( async (req: Request, res: Response) => { // Access validated data from middleware const validatedData = req.validated.body; // Type-safe validated data
// No validation needed - data is guaranteed to be valid const newOccasion = await createOccasion(validatedData);
sendSuccessResponse( req, res, { data: newOccasion, serializerConfig: SerializedOccasion, type: "single", }, { status: HttpStatus.CREATED, }, );
}, );
Validation Error Response:
// Automatic error response from middleware { "errors": [ { "status": "400", "code": "invalid_string", "title": "Validation Error", "detail": "Invalid email", "source": { "pointer": "/body/email" }, "meta": { "requestId": "req-123", "timestamp": "2026-02-23T21:00:00.000Z" } } ] }
Schema Definition:
// occasions.types.ts
export const occasionInsertSchema = createInsertSchema(occasionsSchema).openapi({
title: "CreateOccasion",
description: "Request schema for creating a new occasion",
});
// Generated Zod schema automatically validates:
{
name: z.string().min(1),
description: z.string().optional(),
date: z.date(),
organizerId: z.string().uuid(),
status: z.enum(["created", "upcoming", "active", "passed"]).default("created")
}
Benefits of Middleware Validation:
- Separation of Concerns: Controllers focus on business logic only
- Consistent Validation: All endpoints use the same validation approach
- Type Safety: Controllers receive fully typed, validated data
- Automatic Error Responses: Standardized JSON:API error formatting
- DRY Principle: Validation logic defined once in schemas
- Request Flow: Validation → Controller → Response (clean pipeline)
4. Business Logic Execution
// Create operations
const user = await createUser({ id: uuid(), name, email });
// Find operations
const gift = await findById(id);
if (!gift) {
return sendErrorResponse(req, res, {
status: HttpStatus.NOT_FOUND,
description: "Gift not found",
});
}
// Update/delete operations
await selectGift(processId, giftId);
5. Response Handling
The API uses two main response utility functions that handle JSON:API serialization and consistent response formatting:
sendSuccessResponse<T>()
Purpose: Sends successful responses with automatic JSON:API serialization.
Signature:
export const sendSuccessResponse = <T>(
req: Request,
res: Response,
responseConfig?: JsonApiResponseConfig<T>,
opts?: SuccessResponseOptions,
): Response => {
// Implementation handles serialization and response formatting
};
Parameters:
req: Request- Express request objectres: Response- Express response objectresponseConfig?: JsonApiResponseConfig<T>- Configuration for data serializationopts?: SuccessResponseOptions- Additional options (status, metadata, links)
ResponseConfig Interface:
export type JsonApiResponseConfig<T> = {
type: ResourceType; // "single" | "collection"
data: T | T[]; // Single entity or array
serializerConfig: JsonApiResourceConfig<T>; // Serialization rules
};
SuccessResponseOptions Interface:
export interface SuccessResponseOptions {
status?: StatusConfig; // HTTP status (defaults to 200)
additionalMeta?: Record<string, unknown>; // Additional metadata
links?: ResponseLinks; // Additional response links
}
Usage Examples:
// Single entity response
sendSuccessResponse(
req,
res,
{
data: user,
serializerConfig: SerializedUser,
type: "single",
},
{
status: HttpStatus.CREATED,
additionalMeta: {
version: "1.0",
},
},
);
// Collection response
sendSuccessResponse(
req,
res,
{
data: users,
serializerConfig: SerializedUser,
type: "collection",
},
{
status: HttpStatus.SUCCESS,
additionalMeta: {
total: users.length,
page: 1,
pageSize: 10,
},
},
);
// No content response (204)
sendSuccessResponse(
req,
res,
undefined, // No response config needed for 204
{
status: HttpStatus.NO_CONTENT,
},
);
Automatic Features:
- JSON:API Serialization: Automatically formats data according to JSON:API spec
- Self Links: Automatically adds
selflink to response - Metadata: Includes timestamp and request metadata
- Status Handling: Special handling for 204 No Content responses
Generated Response Structure:
// Single entity
{
"data": {
"id": "uuid",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com"
}
},
"links": {
"self": "https://api.example.com/users/uuid"
},
"meta": {
"timestamp": "2026-02-23T21:00:00.000Z",
"requestId": "req-123",
"version": "1.0" // From additionalMeta
}
}
// Collection
{
"data": [
{ "id": "1", "type": "user", "attributes": {...} },
{ "id": "2", "type": "user", "attributes": {...} }
],
"links": {
"self": "https://api.example.com/users"
},
"meta": {
"timestamp": "2026-02-23T21:00:00.000Z",
"requestId": "req-123",
"total": 2, // From additionalMeta
"page": 1, // From additionalMeta
"pageSize": 10 // From additionalMeta
}
}
sendErrorResponse()
Purpose: Sends error responses following JSON:API error specification.
Signature:
export const sendErrorResponse = (
req: Request,
res: Response,
errorInput: ErrorResponseConfig | JsonApiErrorObject[],
opts?: ErrorResponseOptions,
): Response<JsonApiErrorResponse> => {
// Implementation handles error serialization and formatting
};
Parameters:
req: Request- Express request objectres: Response- Express response objecterrorInput: ErrorResponseConfig | JsonApiErrorObject[]- Error configuration or pre-formatted errorsopts?: ErrorResponseOptions- Additional error options
ErrorResponseConfig Interface:
export type ErrorResponseConfig = Partial<
Pick<StatusConfig, "description" | "title">
> & {
status: StatusConfig; // HTTP status with code
source?: { pointer?: string; method?: string }; // Error source location
details?: Record<string, any>; // Additional error details
};
ErrorResponseOptions Interface:
export interface ErrorResponseOptions {
additionalMeta?: Record<string, unknown>; // Additional metadata
errorId?: string; // Unique error identifier
suppressLogging?: boolean; // Suppress error logging
}
Usage Examples:
// Simple error response
sendErrorResponse(req, res, {
status: HttpStatus.NOT_FOUND,
description: "User not found",
});
// Error with source pointer (for validation errors)
sendErrorResponse(req, res, {
status: HttpStatus.BAD_REQUEST,
title: "Validation Error",
description: "Invalid email format",
source: {
pointer: "/data/attributes/email",
method: "POST",
},
details: {
field: "email",
value: "invalid-email",
expected: "valid email format",
},
});
// Multiple errors (array format)
sendErrorResponse(req, res, [
{
status: "400",
code: "INVALID_EMAIL",
title: "Validation Error",
detail: "Email format is invalid",
source: { pointer: "/data/attributes/email" },
},
{
status: "400",
code: "MISSING_NAME",
title: "Validation Error",
detail: "Name is required",
source: { pointer: "/data/attributes/name" },
},
]);
Generated Response Structure:
// Single error
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "User not found",
"source": {
"pointer": "/users/123"
},
"meta": {
"timestamp": "2026-02-23T21:00:00.000Z",
"requestId": "req-123"
}
}
]
}
// Multiple errors
{
"errors": [
{
"status": "400",
"code": "INVALID_EMAIL",
"title": "Validation Error",
"detail": "Email format is invalid",
"source": { "pointer": "/data/attributes/email" }
},
{
"status": "400",
"code": "MISSING_NAME",
"title": "Validation Error",
"detail": "Name is required",
"source": { "pointer": "/data/attributes/name" }
}
]
}
Automatic Features:
- Status Code Handling: Automatically sets HTTP status from error config
- Error Serialization: Formats errors according to JSON:API spec
- Metadata: Includes timestamp and request metadata
- Logging: Automatically logs errors (can be suppressed)
- Flexible Input: Accepts both config objects and pre-formatted error arrays
Key Benefits
- Consistency: All responses follow JSON:API specification
- Type Safety: Full TypeScript support with generics
- Automatic Serialization: No manual JSON:API formatting needed
- Error Handling: Standardized error response format
- Metadata: Automatic request tracking and timestamps
- Links: Self-linking and custom link support
- Validation: Built-in validation for response configurations
6. Event/Queue Integration
// Optional: Emit events after successful operations
await occasionsQueue.enqueue({
event: OccasionJobs.OccasionCreated,
payload: newOccasion,
metadata: { source: "occasion-controller" },
});
JSON:API Serialization
The API automatically serializes all responses to adhere to the JSON:API specification (v1.0). This ensures consistent response formats across all endpoints and provides clients with a predictable, standardized API experience.
Serialization Process
Automatic Serialization Pipeline
// Controller calls sendSuccessResponse()
sendSuccessResponse(req, res, {
data: user, // Raw entity/entities
type: "single", // Single or collection
serializerConfig: SerializedUser, // Serialization rules
});
// ↓ sendSuccessResponse calls serializeJsonApi()
serializeJsonApi(req, {
responseConfig: { data, type, serializerConfig },
metadata: additionalMeta
});
// ↓ serializeJsonApi calls serializeResource() for each entity
serializeResource(resource, serializerConfig, req);
// ↓ Returns JSON:API compliant response
{
"data": { /* JSON:API resource object */ },
"links": { /* Self links */ },
"meta": { /* Response metadata */ }
}
Resource Serialization
Input Entity:
// Raw database entity
const user = {
id: "uuid-123",
name: "John Doe",
email: "john@example.com",
password: "hashed-password", // Sensitive data
createdAt: "2026-02-23T21:00:00.000Z",
updatedAt: "2026-02-23T21:00:00.000Z",
};
Serializer Configuration:
// users.config.ts
export const SerializedUser: JsonApiResourceConfig<User> = {
type: "user",
attributes: (user: User) => ({
name: user.name,
email: user.email,
createdAt: user.createdAt,
// password excluded for security
}),
resourceMeta: (user: User) => ({
isActive: user.email.includes("@example.com"),
memberSince: user.createdAt,
}),
links: (user: User, req: Request) => ({
self: `${req.protocol}://${req.get("host")}/users/${user.id}`,
profile: `${req.protocol}://${req.get("host")}/users/${user.id}/profile`,
}),
};
JSON:API Output:
{
"data": {
"type": "user",
"id": "uuid-123",
"attributes": {
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2026-02-23T21:00:00.000Z"
},
"meta": {
"isActive": true,
"memberSince": "2026-02-23T21:00:00.000Z"
},
"links": {
"self": "https://api.example.com/users/uuid-123",
"profile": "https://api.example.com/users/uuid-123/profile"
}
},
"links": {
"self": "https://api.example.com/users/uuid-123"
},
"meta": {
"requestId": "req-456",
"timestamp": "2026-02-23T21:00:00.000Z"
}
}
Serialization Configuration
JsonApiResourceConfig Interface
export interface JsonApiResourceConfig<T> {
type: string; // Resource type (e.g., "user", "gift")
attributes: (resource: T) => Record<string, any>; // Attribute mapping function
relationships?: Record<string, RelationshipObject<T>>; // Relationships
resourceMeta?: (resource: T) => Record<string, any>; // Resource-specific metadata
links?: (resource: T, req: Request) => ResourceLinkObject; // Resource links
}
Configuration Examples
Simple Entity:
export const SerializedGift: JsonApiResourceConfig<Gift> = {
type: "gift",
attributes: (gift: Gift) => ({
name: gift.name,
description: gift.description,
price: gift.price,
category: gift.category,
}),
};
Entity with Relationships:
export const SerializedOccasion: JsonApiResourceConfig<Occasion> = {
type: "occasion",
attributes: (occasion: Occasion) => ({
name: occasion.name,
description: occasion.description,
date: occasion.date,
status: occasion.status,
}),
relationships: {
organizer: {
data: (occasion: Occasion) => ({
type: "user",
id: occasion.organizerId,
}),
links: (occasion: Occasion, req: Request) => ({
self: `${req.protocol}://${req.get("host")}/occasions/${occasion.id}/relationships/organizer`,
related: `${req.protocol}://${req.get("host")}/occasions/${occasion.id}/organizer`,
}),
},
participants: {
data: (occasion: Occasion) =>
occasion.participants?.map((p) => ({
type: "user",
id: p.id,
})) || null,
},
},
};
Entity with Custom Metadata and Links:
export const SerializedUser: JsonApiResourceConfig<User> = {
type: "user",
attributes: (user: User) => ({
name: user.name,
email: user.email,
}),
resourceMeta: (user: User) => ({
isActive: user.email.includes("@example.com"),
memberSince: user.createdAt,
lastSeen: user.lastLoginAt,
}),
links: (user: User, req: Request) => ({
self: `${req.protocol}://${req.get("host")}/users/${user.id}`,
profile: `${req.protocol}://${req.get("host")}/users/${user.id}/profile`,
gifts: `${req.protocol}://${req.get("host")}/users/${user.id}/gifts`,
}),
};
Collection Serialization
Collection Response Structure
// Input: Array of entities
const users = [user1, user2, user3];
// Serialization
sendSuccessResponse(
req,
res,
{
data: users,
type: "collection",
serializerConfig: SerializedUser,
},
{
additionalMeta: {
total: users.length,
page: 1,
pageSize: 10,
totalPages: Math.ceil(users.length / 10),
},
},
);
Collection Output:
{
"data": [
{
"type": "user",
"id": "uuid-1",
"attributes": { "name": "John", "email": "john@example.com" },
"links": { "self": "https://api.example.com/users/uuid-1" }
},
{
"type": "user",
"id": "uuid-2",
"attributes": { "name": "Jane", "email": "jane@example.com" },
"links": { "self": "https://api.example.com/users/uuid-2" }
},
{
"type": "user",
"id": "uuid-3",
"attributes": { "name": "Bob", "email": "bob@example.com" },
"links": { "self": "https://api.example.com/users/uuid-3" }
}
],
"links": {
"self": "https://api.example.com/users?page=1",
"first": "https://api.example.com/users?page=1",
"last": "https://api.example.com/users?page=1",
"prev": null,
"next": null
},
"meta": {
"requestId": "req-789",
"timestamp": "2026-02-23T21:00:00.000Z",
"total": 3,
"page": 1,
"pageSize": 10,
"totalPages": 1
}
}
Error Serialization
Error Response Structure
// Error input
sendErrorResponse(req, res, {
status: HttpStatus.BAD_REQUEST,
title: "Validation Error",
description: "Invalid email format",
source: {
pointer: "/data/attributes/email",
method: "POST",
},
details: {
field: "email",
value: "invalid-email",
expected: "valid email format",
},
});
Error Output:
{
"errors": [
{
"id": "error-uuid-123",
"status": "400",
"code": "BAD_REQUEST",
"title": "Validation Error",
"detail": "Invalid email format",
"source": {
"pointer": "/data/attributes/email",
"method": "POST"
},
"meta": {
"requestId": "req-456",
"timestamp": "2026-02-23T21:00:00.000Z",
"field": "email",
"value": "invalid-email",
"expected": "valid email format"
}
}
]
}
JSON:API Compliance Features
1. Resource Objects
{
"type": "user", // Required: Resource type
"id": "uuid-123", // Required: Resource ID
"attributes": { ... }, // Optional: Resource attributes
"relationships": { ... }, // Optional: Resource relationships
"links": { ... }, // Optional: Resource links
"meta": { ... } // Optional: Resource metadata
}
2. Top-Level Response
{
"data": { ... }, // Required: Primary data
"errors": [ ... ], // Optional: Error objects (mutually exclusive with data)
"meta": { ... }, // Optional: Response metadata
"links": { ... }, // Optional: Response links
"included": [ ... ] // Optional: Included resources
}
3. Automatic Features
- Type Safety: TypeScript generics ensure type consistency
- ID Handling: Automatic ID extraction and validation
- Attribute Filtering: Secure attribute exposure control
- Link Generation: Automatic self-link generation
- Metadata: Automatic request tracking and timestamps
- Error Formatting: JSON:API error object compliance
4. Security Considerations
- Attribute Filtering: Sensitive data excluded via serializer config
- ID Validation: Ensures all resources have valid IDs
- Type Validation: Prevents type injection attacks
- Link Sanitization: Safe URL generation
Benefits of JSON:API Serialization
For API Consumers
- Consistency: Predictable response structure across all endpoints
- Self-Documentation: Clear resource types and relationships
- Hypermedia: Built-in navigation via links
- Efficiency: Sparse fieldsets and inclusion support
- Standardization: Industry-recognized specification
For Developers
- Type Safety: Full TypeScript support
- Automatic Formatting: No manual JSON:API formatting needed
- Security: Controlled attribute exposure
- Maintainability: Centralized serialization logic
- Extensibility: Easy to add new resources and relationships
For the System
- Performance: Optimized serialization pipeline
- Caching: Consistent response formats enable better caching
- Monitoring: Standardized metadata for tracking
- Testing: Predictable response structures
- Documentation: Self-documenting API structure
This automatic JSON:API serialization ensures that all API responses are consistent, secure, and compliant with industry standards while providing developers with powerful configuration options for customizing resource representation.
JSON:API Standard
All responses follow JSON:API specification with automatic serialization:
// Single entity response
{
"data": {
"id": "uuid",
"type": "user",
"attributes": {
"name": "John Doe",
"email": "john@example.com"
}
},
"meta": {
"timestamp": "2026-02-23T20:57:00.000Z"
}
}
// Collection response
{
"data": [
{ "id": "1", "type": "user", "attributes": {...} },
{ "id": "2", "type": "user", "attributes": {...} }
],
"meta": {
"total": 2,
"timestamp": "2026-02-23T20:57:00.000Z"
}
}
// Error response
{
"errors": [
{
"status": "404",
"title": "Not Found",
"detail": "Entity not found"
}
]
}
HTTP Status Codes
- 200 SUCCESS: Successful GET requests
- 201 CREATED: Successful POST (create) requests
- 204 NO CONTENT: Successful DELETE or operations with no response body
- 400 BAD REQUEST: Invalid input/validation errors
- 404 NOT FOUND: Resource not found
- 500 INTERNAL SERVER ERROR: Server errors
Controller Examples
1. Create Controller (POST)
// users/controllers/create-user.controller.ts
export const createUserController = asyncHandler(
async (req: Request, res: Response) => {
const { name, email } = req.body ?? {};
const user = await createUser({
id: uuid(),
name: name as string,
email: email as string,
});
sendSuccessResponse(
req,
res,
{
data: user,
serializerConfig: SerializedUser,
type: "single",
},
{
status: HttpStatus.CREATED,
},
);
},
);
2. Get Single Controller (GET)
// gifts/controllers/get-gift.controller.ts
export const getGiftController = asyncHandler(
async (req: Request, res: Response) => {
const id = req.params.id as string;
const gift = await findById(id);
if (!gift) {
return sendErrorResponse(req, res, {
status: HttpStatus.NOT_FOUND,
description: "Gift not found",
});
}
sendSuccessResponse(
req,
res,
{
data: gift,
serializerConfig: SerializedGift,
type: "single",
},
{
status: HttpStatus.SUCCESS,
},
);
},
);
3. Get Collection Controller (GET)
// users/controllers/get-users.controller.ts
export const getUsersController = asyncHandler(
async (req: Request, res: Response) => {
const users = await findUsers();
const total = users.length;
return sendSuccessResponse(
req,
res,
{
data: users,
serializerConfig: SerializedUser,
type: "collection",
},
{
status: HttpStatus.SUCCESS,
additionalMeta: {
total,
},
},
);
},
);
4. Action Controller (POST/PUT)
// decisions/controllers/select-gift.controller.ts
export const selectGiftOptionController = asyncHandler(
async (req: Request, res: Response) => {
const { processId, giftId } = req.params;
// Validation
if (!processId || Array.isArray(processId)) {
return sendErrorResponse(req, res, {
status: HttpStatus.BAD_REQUEST,
description: "Process ID is required",
});
}
// Execute action
await selectGift(processId, giftId);
// Return 204 No Content
return sendSuccessResponse(req, res, undefined, {
status: HttpStatus.NO_CONTENT,
});
},
);
5. Controller with Event Emission
// occasions/controllers/create-occasions.controller.ts
export const createOccasionController = asyncHandler(
async (req: Request, res: Response) => {
const reqBody = req.body;
const newOccasion = await createOccasion(reqBody);
sendSuccessResponse(
req,
res,
{
data: newOccasion,
serializerConfig: SerializedOccasion,
type: "single",
},
{
status: HttpStatus.CREATED,
},
);
// Emit event after successful creation
await occasionsQueue.enqueue({
event: OccasionJobs.OccasionCreated,
payload: newOccasion,
metadata: { source: "occasion-controller" },
});
},
);
Best Practices
1. Consistent Error Handling
- Always validate required parameters
- Use appropriate HTTP status codes
- Provide clear error descriptions
2. Input Validation
- Validate route parameters
- Check for array values when expecting strings
- Use TypeScript types for type safety
3. Response Consistency
- Always use
sendSuccessResponseandsendErrorResponse - Include proper serializer configuration
- Use appropriate response types ("single" vs "collection")
4. Event Emission
- Emit events after successful operations
- Include relevant metadata for tracking
- Use event constants from config files
5. Async Operations
- Always wrap controllers in
asyncHandler - Use await for all async operations
- Handle promise rejections properly
6. Separation of Concerns
- Controllers handle HTTP concerns only
- Business logic goes to operations
- Database operations go to repositories
- Event handling goes to event handlers
File Organization
src/feature/[feature-name]/
├── controllers/
│ ├── create-[feature].controller.ts
│ ├── get-[feature].controller.ts
│ ├── get-[feature]s.controller.ts
│ ├── update-[feature].controller.ts
│ └── delete-[feature].controller.ts
├── operations/
│ ├── [feature].create.ts
│ ├── [feature].find.ts
│ ├── [feature].update.ts
│ └── [feature].delete.ts
├── [feature].config.ts
├── [feature].types.ts
└── workers/
├── [feature].events.ts
├── [feature].queue.ts
└── [feature].workers.ts
This structure ensures clear separation of concerns and consistent patterns across all features.