Skip to main content

Wrkbelt API Architecture and Patterns

Introduction

The Wrkbelt API is built on a set of consistent patterns and architectural principles that enable scalable, maintainable, and type-safe development. This documentation outlines the core patterns, design decisions, and best practices that serve as the foundation for our API implementation.

Our API follows a multi-layered architecture that enforces separation of concerns, leverages TypeScript's type system across the full stack, and implements RESTful services with standardized patterns. This guide will help engineers understand how these patterns work together to create a robust and scalable API.

Core Architectural Principles

Multi-Layered Architecture

The Wrkbelt API follows a three-layer architecture pattern:

  1. Controller Layer: Handles HTTP requests/responses and routes to appropriate services
  2. Service Layer: Contains business logic and coordinates between controllers and repositories
  3. Repository Layer: Manages data access and persistence with MongoDB/Mongoose

This separation ensures:

  • Single Responsibility: Each component has a clear and focused role
  • Testability: Layers can be tested in isolation with appropriate mocks
  • Maintainability: Changes in one layer don't necessitate changes in others
  • Scalability: Components can be scaled independently based on demand

Multi-Layered Architecture Diagram

Technology Foundation

The API is built on:

  • NestJS: A progressive Node.js framework for building efficient and scalable server-side applications
  • MongoDB: Document database with Mongoose ODM for schema definition and validation
  • TypeScript: For type-safety, intellisense, and better developer experience

Key Design Patterns

1. Shared Type System

A unified type system forms the foundation of our full-stack architecture. Types defined in @wrkbelt/shared/types-data are used by both the API and UI, ensuring consistency across the application.

// Example of a shared entity type in libs/shared/types-data
export type BaseEntity = {
_id: string;
createdAt: Date;
updatedAt: Date;
};

Benefits:

  • Type Safety: Prevents runtime errors by catching type mismatches at compile time
  • Single Source of Truth: Changes to entity structures are automatically reflected in both UI and API
  • Developer Experience: Intellisense provides autocompletion and documentation

2. Repository Pattern

The repository pattern abstracts data access logic behind a consistent interface, implemented with a BaseRepository class that all entity repositories extend.

@Injectable()
export abstract class BaseRepository<
TEntity extends BaseEntity,
TDocument extends HydratedDocument<TEntity>
> implements IBaseRepository<TEntity, TDocument> {
// Standard implementation with normalization and transaction support
}

Key features:

  • Entity Normalization: Transforms MongoDB documents into domain entities
  • Transaction Management: Provides a unified approach to database transactions
  • CRUD Operations: Standardized methods for common data operations

3. RESTful Resource Modules

Each domain entity is represented as a resource module with standardized components:

  • Controller: Defines RESTful endpoints with appropriate decorators
  • Service: Implements business logic and coordinates data access
  • Repository: Handles data persistence and retrieval
  • Schema: Defines the MongoDB document structure
  • DTOs: Define the shape of data for input/output

Example module structure (Permission module):

permission/
├── dtos/
│ ├── create-permission.dto.ts
│ └── update-permission.dto.ts
├── permission.controller.ts
├── permission.module.ts
├── permission.repository.ts
├── permission.schema.ts
└── permission.service.ts

4. Centralized Route Management

All API routes are defined in a shared library, making them available to both the UI and API:

// Example from libs/shared/utils-core-logic/src/lib/routes/api.routes.ts
export const API_RESOURCES = {
PERMISSIONS: 'permissions',
// Other resources...
} as const;

export const API_ACTIONS = {
PERMISSION: {
BY_ID: ':id',
// Other actions...
},
// Other resource actions...
} as const;

export const API_ROUTES = {
PERMISSION: {
GET_ALL: createRoute(API_RESOURCES.PERMISSIONS, undefined, 'api'),
GET_BY_ID: createRoute(API_RESOURCES.PERMISSIONS, API_ACTIONS.PERMISSION.BY_ID, 'api'),
// Other routes...
},
// Other resource routes...
};

Benefits:

  • Type Safety: Routes are type-checked at compile time
  • Consistency: Ensures consistent URL patterns across resources
  • Single Source of Truth: UI and API share the same route definitions

Implementation Examples

Controller Implementation

@ApiTags(capitalizeFirstCharacter(API_RESOURCES.PERMISSIONS))
@ApiCookieAuth()
@Controller(API_RESOURCES.PERMISSIONS)
export class PermissionController {
constructor(private readonly permissionService: PermissionService) {}

@Get()
@RequirePermissions(AllPermissions.VIEW_PERMISSIONS)
async findAll() {
return this.permissionService.findAll();
}

@Post()
@RequirePermissions(AllPermissions.CREATE_PERMISSION)
async create(@Body() createPermissionDto: CreatePermissionDto) {
return this.permissionService.create(createPermissionDto);
}

// Other endpoints...
}

Service Implementation

@Injectable()
export class PermissionService {
private readonly logger = new Logger(PermissionService.name);

constructor(private readonly permissionRepository: PermissionRepository) {}

async findAll(query: FilterQuery<PermissionDocument> = {}) {
return this.permissionRepository.find(query);
}

async create(createPermissionDto: CreatePermissionDto): Promise<Permission> {
// Implementation with business logic
}

// Other service methods...
}

Repository Implementation

@Injectable()
export class PermissionRepository
extends BaseRepository<Permission, PermissionDocument>
implements IPermissionRepository {

constructor(
@InjectModel(PermissionSchema.name) permissionModel: Model<PermissionDocument>,
@InjectConnection() connection: Connection
) {
super(permissionModel, connection);
}

// Custom repository methods...
}

Authentication and Authorization

The API implements a comprehensive authentication and authorization system:

  • Session-based Authentication: Redis-backed sessions for stateful authentication
  • Guard-based Protection: Routes are protected by default with NestJS guards
  • Permission-based Authorization: Fine-grained access control with the @RequirePermissions decorator
  • Multi-tenancy Support: Users can belong to multiple organizations with different permissions

See the Authentication Pattern documentation for details.

Best Practices

1. Controller Best Practices

  • Use appropriate HTTP methods (GET, POST, PUT, DELETE)
  • Apply validation decorators on DTOs
  • Document endpoints with Swagger annotations
  • Keep controllers thin and focused on request/response handling

2. Service Best Practices

  • Implement business logic in services, not controllers or repositories
  • Use dependency injection for composing functionality
  • Handle errors with appropriate exception filters
  • Log important operations and errors

3. Repository Best Practices

  • Always normalize documents to entities before returning them
  • Use transactions for operations that modify multiple documents
  • Implement custom interfaces for better code organization
  • Avoid including business logic in repositories

4. General Best Practices

  • Follow consistent naming conventions
  • Write tests for all layers
  • Document public APIs and internal design decisions
  • Apply SOLID principles throughout the codebase

Next Steps

To dive deeper into specific aspects of the API architecture, continue to: