Web Layer
The Web Layer is the outermost layer of 2KRIKA's clean architecture, handling HTTP requests, WebSocket connections, and dependency injection. Built with NestJS, it serves as the entry point for all external interactions.
Overview
Located in the web/ directory, this layer:
- Handles HTTP requests and responses via controllers
- Manages WebSocket connections for real-time features
- Performs dependency injection using NestJS modules
- Applies guards and filters for authentication and error handling
- Transforms DTOs to API responses with Swagger documentation
- Orchestrates use cases without implementing business logic
Controllers
Controllers handle HTTP endpoints and delegate to use cases.
Controller Anatomy
// web/users/users.controller.ts
@Controller("users")
@UseGuards(AuthGuard)
export class UsersController {
constructor(
private readonly userRepository: UserRepository,
private readonly notifier: Notifier,
private readonly storage: StorageService,
) {}
@Post()
@NoAuth() // Public endpoint
@ApiCreatedResponse({ type: UserResponse })
@ApiBadRequestResponse()
async create(@Body() body: UserCreateSchema) {
return await userUseCases.create(
body,
this.userRepository,
this.notifier,
logger,
);
}
@Get("me")
@ApiOkResponse({ type: MeResponse })
async getMe(@Req() request: AuthenticatedRequest) {
return await userUseCases.getMe(
request.user.id,
this.userRepository,
this.orderRepository,
this.serviceRepository,
this.competencyRepository,
this.commentRepository,
this.accountRepository,
);
}
@Patch("me")
@ApiOkResponse({ type: UserResponse })
@ApiBadRequestResponse()
async updateMe(
@Req() request: AuthenticatedRequest,
@Body() body: UserUpdateSchema,
) {
return await userUseCases.updateMe(
body,
this.userRepository,
request.user,
);
}
@Delete("me")
@HttpCode(HttpStatus.NO_CONTENT)
@ApiNoContentResponse()
async deleteMe(@Req() request: AuthenticatedRequest) {
await userUseCases.deleteMe(request.user.id, this.userRepository);
}
}
Key characteristics: - Decorators – NestJS routing and metadata - Dependency injection – Repositories injected via constructor - Minimal logic – Delegates to use cases - Swagger documentation – API response types - Guards – Authentication and authorization
File Upload Example
// web/users/users.controller.ts
@Post("me/profile-picture")
@UseInterceptors(FileInterceptor("file"))
@ApiConsumes("multipart/form-data")
@ApiBody({
schema: {
type: "object",
properties: {
file: {
type: "string",
format: "binary",
},
},
},
})
async uploadProfilePicture(
@Req() request: AuthenticatedRequest,
@UploadedFile() file: Express.Multer.File,
) {
const uploadFile = multerToFile(file);
return await userUseCases.uploadProfilePicture(
request.user.id,
uploadFile,
this.userRepository,
this.storage,
);
}
Key features:
- File interceptor – Handles multipart/form-data
- Multer integration – File upload middleware
- File conversion – Transform to domain File type
- Swagger support – Documents file upload
Query Parameters
// web/orders/orders.controller.ts
@Get()
@ApiOkResponse({ type: OrdersListResponse })
async list(
@Req() request: AuthenticatedRequest,
@Query() query: ListOrdersQuery,
) {
return await orderUseCases.list(
query,
request.user,
this.orderRepository,
this.userRepository,
);
}
Query DTO:
// web/orders/orders.controller.types.ts
export class ListOrdersQuery extends PaginationQuery {
@ApiPropertyOptional()
state?: OrderState;
@ApiPropertyOptional()
search?: string;
@ApiPropertyOptional({ type: [String] })
orderBy?: string[];
}
export class PaginationQuery {
@ApiPropertyOptional({ default: 1 })
page?: number = 1;
@ApiPropertyOptional({ default: 20 })
pageSize?: number = 20;
}
Guards
Guards control access to routes based on authentication and authorization.
Authentication Guard
// web/auth/auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private logger: FileLogger,
private userRoleRepository: UserRoleSqlRepository,
private userRepository: UserRepository,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check if route is public
const isPublic = this.reflector.get<boolean>(
"public",
context.getHandler(),
);
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
// Allow public routes without token
if (isPublic && !token) {
return true;
}
if (!token) {
throw new UnauthorizedException();
}
try {
// Verify JWT token
const payload = await this.authService.verifyAccessToken(token);
// Load user from database
const user = await this.userRepository.get(payload.id);
// Fetch user permissions
await this.userRoleRepository.getAllPermissions(user);
// Attach user to request
request["user"] = user;
return true;
} catch (e: any) {
this.logger.error(`Auth guard error: ${e.message}`);
throw new UnauthorizedException();
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
Key features:
- JWT verification – Validates access tokens
- User loading – Fetches user from database
- Permission loading – Attaches permissions to user object
- Public routes – Supports @NoAuth() decorator
- Request augmentation – Adds user to request object
Public Route Decorator
// web/auth/auth.guard.ts
export const NoAuth = () => SetMetadata("public", true);
// Usage in controller
@Post("register")
@NoAuth()
async register(@Body() body: any) {
// Public endpoint
}
Exception Filters
Filters catch exceptions and transform them into HTTP responses.
Validation Error Filter
// web/http-exception.filter.ts
@Catch(ValidationError)
export class BadRequestExceptionFilter implements ExceptionFilter {
catch(exception: ValidationError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = 400;
response.status(status).json(exception.json());
}
}
Response format:
Not Found Filter
// web/http-exception.filter.ts
@Catch(NotFound)
export class NotFoundExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = 404;
response.status(status).json({
statusCode: status,
message: exception.message,
path: request.url,
});
}
}
Forbidden Error Filter
// web/http-exception.filter.ts
@Catch(ForbiddenError)
export class ForbiddenExceptionFilter implements ExceptionFilter {
catch(exception: ForbiddenError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = 403;
response.status(status).json({
statusCode: status,
message: exception.message,
path: request.url,
});
}
}
Registering Filters
// web/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Register global exception filters
app.useGlobalFilters(
new BadRequestExceptionFilter(),
new NotFoundExceptionFilter(),
new ForbiddenExceptionFilter(),
);
await app.listen(3000);
}
Dependency Injection
NestJS modules configure dependency injection.
Module Structure
// web/users/users.module.ts
@Module({
imports: [AuthModule], // Import other modules
providers: [
// Notifier service
EmailNotifier,
// Repository implementations
IdentitySqlRepository,
{ provide: ServiceRepository, useClass: ServiceSqlRepository },
{ provide: IdentityRepository, useClass: IdentitySqlRepository },
{ provide: OrderRepository, useClass: OrderSqlRepository },
{ provide: AccountRepository, useClass: AccountSqlRepository },
{ provide: CompetencyRepository, useClass: CompetencySqlRepository },
{ provide: CommentRepository, useClass: CommentSqlRepository },
{ provide: UserRoleRepository, useClass: UserRoleSqlRepository },
],
controllers: [UsersController, KYCController],
})
export class UsersModule {}
Key features: - Interface binding – Domain interfaces → concrete implementations - Service registration – Notifiers, storage, logging - Module imports – Reuse dependencies from other modules - Controller registration – HTTP endpoints
Root Module
// web/app.module.ts
@Module({
imports: [
AuthModule,
UsersModule,
ServicesModule,
OrdersModule,
StatsModule,
ConfigModule,
AccountsModule,
ScheduleModule.forRoot(),
],
})
export class AppModule {}
Provider Patterns
Class provider:
providers: [UserSqlRepository]
// Shorthand for:
// { provide: UserSqlRepository, useClass: UserSqlRepository }
Interface provider:
providers: [
{ provide: UserRepository, useClass: UserSqlRepository }
]
// Binds abstract class to concrete implementation
Factory provider:
providers: [
{
provide: StorageService,
useFactory: (fileSystem: FileSystem, uploadRepo: UploadRepository) => {
return new StorageService(fileSystem, uploadRepo);
},
inject: [LocalFileSystem, UploadRepository],
},
]
WebSocket Gateway
Real-time communication using Socket.IO.
Chat Gateway
// web/orders/chat.gateway.ts
@WebSocketGateway({
cors: {
origin: "*",
},
})
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(
private readonly chatRepository: ChatRepository,
private readonly userRepository: UserRepository,
private readonly authService: AuthService,
) {}
async handleConnection(client: Socket) {
try {
// Authenticate WebSocket connection
const token = client.handshake.auth.token;
const payload = await this.authService.verifyAccessToken(token);
const user = await this.userRepository.get(payload.id);
// Store user in socket metadata
client.data.user = user;
// Join user's personal room
client.join(`user:${user.id}`);
console.log(`User ${user.id} connected`);
} catch (error) {
client.disconnect();
}
}
handleDisconnect(client: Socket) {
const user = client.data.user;
if (user) {
console.log(`User ${user.id} disconnected`);
}
}
@SubscribeMessage("chat:join")
async handleJoinChat(
@ConnectedSocket() client: Socket,
@MessageBody() data: { chatId: string },
) {
const user = client.data.user;
// Verify user has access to chat
const chat = await this.chatRepository.get(data.chatId);
const isMember = await chatUseCases.isMember(
user.id,
chat.id,
this.chatRepository,
);
if (!isMember) {
throw new WsException("Forbidden");
}
// Join chat room
client.join(`chat:${data.chatId}`);
return { success: true };
}
@SubscribeMessage("chat:send_message")
async handleSendMessage(
@ConnectedSocket() client: Socket,
@MessageBody() data: { chatId: string; content: string },
) {
const user = client.data.user;
// Create message via use case
const message = await chatUseCases.sendMessage(
data.chatId,
data.content,
user,
this.chatRepository,
);
// Broadcast to chat room
this.server.to(`chat:${data.chatId}`).emit("chat:new_message", {
id: message.id,
chatId: message.chatId,
senderId: message.senderId,
content: message.content,
createdAt: message.createdAt,
});
return { success: true };
}
@SubscribeMessage("chat:typing")
async handleTyping(
@ConnectedSocket() client: Socket,
@MessageBody() data: { chatId: string },
) {
const user = client.data.user;
// Broadcast typing indicator (excluding sender)
client.to(`chat:${data.chatId}`).emit("chat:user_typing", {
chatId: data.chatId,
userId: user.id,
userName: user.fullName,
});
}
}
Key features:
- Authentication – Verify JWT on connection
- Room management – Join/leave chat rooms
- Event handlers – @SubscribeMessage decorators
- Broadcasting – Emit to specific rooms
- User context – Store authenticated user in socket
API Response Types
Swagger Documentation
// web/users/users.controller.types.ts
export class UserResponse {
@ApiProperty()
id: string;
@ApiProperty()
email: string;
@ApiProperty()
fullName: string;
@ApiProperty()
username: string;
@ApiProperty()
isActive: boolean;
@ApiProperty()
isKYCVerified: boolean;
@ApiProperty()
createdAt: Date;
}
export class UsersListResponse {
@ApiProperty({ type: [UserResponse] })
results: UserResponse[];
@ApiProperty()
count: number;
@ApiProperty()
page: number;
@ApiProperty()
pageSize: number;
}
export class UserCreateSchema {
@ApiProperty()
fullName: string;
@ApiProperty()
email: string;
@ApiProperty()
password: string;
@ApiPropertyOptional()
username?: string;
}
Benefits: - Auto-generated OpenAPI spec – Swagger UI documentation - Type safety – TypeScript interfaces - Validation – Can be combined with class-validator - Client generation – Generate API clients from spec
Middleware
CORS Configuration
Request Logging
Application Bootstrap
// web/main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
import { Config } from "@/app/config";
import {
BadRequestExceptionFilter,
ForbiddenExceptionFilter,
NotFoundExceptionFilter,
} from "./http-exception.filter";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// CORS
app.enableCors({
origin: process.env.CORS_ORIGIN || "*",
credentials: true,
});
// Global prefix
app.setGlobalPrefix("api/v1");
// Exception filters
app.useGlobalFilters(
new BadRequestExceptionFilter(),
new NotFoundExceptionFilter(),
new ForbiddenExceptionFilter(),
);
// Swagger documentation
const config = new DocumentBuilder()
.setTitle("2KRIKA API")
.setDescription("The 2KRIKA API documentation")
.setVersion("1.0")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup("api/docs", app, document);
// Load configuration
await Config.loads();
// Start server
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`Application is running on: http://localhost:${port}`);
}
bootstrap();
Web Module Organization
web/
├── accounts/ # Account endpoints
│ ├── accounts.module.ts
│ ├── accounts.controller.ts
│ └── accounts.controller.types.ts
├── auth/ # Authentication
│ ├── auth.module.ts
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── auth.guard.ts
│ └── auth.controller.types.ts
├── orders/ # Order endpoints
│ ├── orders.module.ts
│ ├── orders.controller.ts
│ ├── orders.controller.types.ts
│ ├── payments.controller.ts
│ ├── chat.controller.ts
│ ├── chat.gateway.ts (WebSocket)
│ └── comments.controller.ts
├── services/ # Service endpoints
│ ├── services.module.ts
│ ├── services.controller.ts
│ ├── services.controller.types.ts
│ └── categories.controller.ts
├── users/ # User endpoints
│ ├── users.module.ts
│ ├── users.controller.ts
│ ├── users.controller.types.ts
│ ├── kyc.controller.ts
│ └── kyc.controller.types.ts
├── stats/ # Statistics endpoints
│ ├── stats.module.ts
│ └── stats.controller.ts
├── config/ # Configuration endpoints
│ ├── config.module.ts
│ └── config.controller.ts
├── shared/ # Shared web utilities
│ ├── renderers/
│ │ └── csv.ts
│ └── swagger/
│ └── types.ts
├── app.module.ts # Root module
├── main.ts # Application entry point
├── http-exception.filter.ts
├── types.ts # Shared types
└── utils.ts # Utility functions
Best Practices
✅ Do
- Keep controllers thin – Delegate to use cases
- Use dependency injection – Inject repositories and services
- Document with Swagger – Add API response types
- Apply guards – Protect routes with authentication
- Handle errors – Use exception filters
- Validate input – Use DTOs and validators
❌ Don't
- Don't put business logic in controllers – It belongs in use cases
- Don't bypass guards – Always authenticate when needed
- Don't expose internal errors – Map to user-friendly messages
- Don't skip Swagger docs – API documentation is essential
- Don't hardcode URLs – Use environment variables
- Don't forget CORS – Configure for your frontend
Testing Controllers
// web/users/users.controller.spec.ts
describe("UsersController", () => {
let controller: UsersController;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockNotifier: jest.Mocked<Notifier>;
beforeEach(async () => {
mockUserRepository = createMockUserRepository();
mockNotifier = createMockNotifier();
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{ provide: UserRepository, useValue: mockUserRepository },
{ provide: Notifier, useValue: mockNotifier },
],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it("should create a user", async () => {
const body = {
fullName: "John Doe",
email: "john@example.com",
password: "password123",
};
const result = await controller.create(body);
expect(result).toHaveProperty("id");
expect(result.email).toBe("john@example.com");
expect(mockUserRepository.add).toHaveBeenCalled();
});
});
Summary
The Web Layer:
- Handles HTTP and WebSocket communication
- Manages dependency injection using NestJS modules
- Applies authentication and authorization via guards
- Transforms exceptions to HTTP responses using filters
- Documents APIs with Swagger/OpenAPI
- Delegates business logic to use cases
This layer is the entry point to the application but contains minimal logic, keeping the architecture clean and testable.