Salve, experienced legionary! Now that you know the architecture of our Roman Empire, it's time to learn the details of how controllers work - the centurions of individual structures. They receive orders from the outside world and manage operations in their domains!
Controllers are classes responsible for handling HTTP requests and returning responses. They are like centurions of different Empire structures - each specializes in their field and knows how to react to different situations.
1import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
2
3@Controller('legion') // Prefix for all routes: /legion
4export class LegionController {
5 constructor(private readonly legionService: LegionService) {}
6
7 // GET /legion - list the entire legion
8 @Get()
9 getAllLegionaries() {
10 return this.legionService.findAll();
11 }
12
13 // GET /legion/5 - specific legionary
14 @Get(':id')
15 getLegionary(@Param('id') id: string) {
16 return this.legionService.findById(+id);
17 }
18
19 // POST /legion - recruit a new legionary
20 @Post()
21 recruitLegionary(@Body() newLegionary: CreateLegionaryDto) {
22 return this.legionService.recruit(newLegionary);
23 }
24
25 // PUT /legion/5 - promote a legionary
26 @Put(':id/promote')
27 promoteLegionary(@Param('id') id: string, @Body() promotion: PromotionDto) {
28 return this.legionService.promote(+id, promotion);
29 }
30
31 // DELETE /legion/5 - remove from the legion (unfortunately...)
32 @Delete(':id')
33 dismissLegionary(@Param('id') id: string) {
34 return this.legionService.dismiss(+id);
35 }
36}Each decorator corresponds to different types of signals a province can send:
1@Controller('tributes')
2export class TributesController {
3
4 // GET - retrieve information (like a status query)
5 @Get()
6 getAllTributes() {
7 return 'List of all collected tributes';
8 }
9
10 // POST - create a new resource (like reporting a new tribute)
11 @Post()
12 reportNewTribute(@Body() tribute: CreateTributeDto) {
13 return \`New tribute reported: \${tribute.name}\`1 }
2
3 // PUT - update the entire tribute (like a complete registry update)
4 @Put(':id')
5 updateTribute(@Param('id') id: string, @Body() tribute: UpdateTributeDto) {
6 return \`Tribute \${id} completely updated\`;
7 }
8
9 // PATCH - partial update (like a correction in the registry)
10 @Patch(':id')
11 modifyTribute(@Param('id') id: string, @Body() changes: Partial<UpdateTributeDto>) {
12 return \`Tribute \${id} partially modified\`;
13 }
14
15 // DELETE - remove tribute (like marking a tribute as collected)
16 @Delete(':id')
17 markTributeAsCollected(@Param('id') id: string) {
18 return \`Tribute \${id} marked as collected and removed from active list\`;
19 }
20}1@Controller('forum')
2export class ForumController {
3
4 // /forum/ballista/5/attack - specific ballista
5 @Post(':section/:id/:action')
6 controlForumSection(
7 @Param('section') section: string,
8 @Param('id') id: string,
9 @Param('action') action: string,
10 ) {
11 return \`Executing \${action} on \${section} #\${id}\`;
12 }
13
14 // Parameter validation
15 @Get('legion/:legionaryId')
16 getLegionaryInfo(@Param('legionaryId', ParseIntPipe) legionaryId: number) {
17 // ParseIntPipe automatically converts and validates
18 return \`Information about legionary with ID: \${legionaryId}\`;
19 }
20}1@Controller('engineering')
2export class EngineeringController {
3
4 // /engineering/route?from=rome&to=province&speed=fast
5 @Get('route')
6 calculateRoute(
7 @Query('from') fromCity: string,
8 @Query('to') toDestination: string,
9 @Query('speed') speed?: string,
10 ) {
11 return {
12 route: \`From \${fromCity} to \${toDestination}\`,
13 speed: speed || 'normal',
14 estimatedTime: this.calculateTime(fromCity, toDestination, speed),
15 };
16 }
17
18 // Query parameter validation
19 @Get('search')
20 searchTributes(
21 @Query('lat', ParseFloatPipe) latitude: number,
22 @Query('lng', ParseFloatPipe) longitude: number,
23 @Query('radius', new ParseIntPipe({ optional: true })) radius?: number,
24 ) {
25 return \`Searching for tributes at coordinates: \${latitude}, \${longitude}\`;
26 }
27}1@Controller('campaign')
2export class CampaignController {
3
4 // Single object in body
5 @Post('attack')
6 initiateAttack(@Body() attackPlan: AttackPlanDto) {
7 return \`Attack initiated with \${attackPlan.weaponType} against \${attackPlan.target}\`;
8 }
9
10 // Body validation and transformation
11 @Post('formation')
12 setCampaignFormation(@Body(ValidationPipe) formation: CampaignFormationDto) {
13 return \`Campaign formation set: \${formation.type}\`;
14 }
15
16 // Partial update with body
17 @Patch('strategy')
18 updateStrategy(@Body() updates: Partial<CampaignStrategyDto>) {
19 return \`Strategy updated with: \${Object.keys(updates).join(', ')}\`;
20 }
21}1@Controller('communication')
2export class CommunicationController {
3
4 @Post('message')
5 sendMessage(
6 @Body() message: MessageDto,
7 @Headers('authorization') authToken: string,
8 @Headers('forum-id') forumId: string,
9 ) {
10 return \`Message sent from forum \${forumId}\`;
11 }
12
13 // All headers at once
14 @Get('status')
15 getForumStatus(@Headers() headers: Record<string, string>) {
16 return {
17 requestOrigin: headers['user-agent'],
18 timestamp: new Date().toISOString(),
19 };
20 }
21}1import { IsString, IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
2
3export class CreateLegionaryDto {
4 @IsString()
5 name: string;
6
7 @IsEnum(['consul', 'engineer', 'ballista-operator', 'cook', 'medicus'])
8 specialization: string;
9
10 @IsNumber()
11 @Min(1)
12 @Max(10)
13 experienceLevel: number;
14
15 @IsOptional()
16 @IsString()
17 backstory?: string;
18}
19
20export class AttackPlanDto {
21 @IsString()
22 target: string;
23
24 @IsEnum(['ballista', 'pilum', 'gladius', 'testudo'])
25 weaponType: string;
26
27 @IsNumber()
28 @Min(1)
29 @Max(100)
30 intensity: number;
31}1@Controller('exploration')
2export class ExplorationController {
3
4 @Get('province/:id')
5 async exploreProvince(@Param('id') provinceId: string) {
6 // Simulation of long-running exploration
7 const tributeMap = await this.mapService.getTributeMap(provinceId);
8 const weather = await this.weatherService.checkConditions(provinceId);
9 const legion = await this.legionService.getAvailableExplorers();
10
11 return {
12 province: provinceId,
13 tributePossibility: tributeMap.likelihood,
14 weather: weather.condition,
15 readyLegion: legion.length,
16 };
17 }
18
19 @Post('campaign')
20 async launchCampaign(@Body() campaign: CampaignDto) {
21 try {
22 const result = await this.campaignService.launch(campaign);
23 return { success: true, campaign: result };
24 } catch (error) {
25 throw new BadRequestException(\`Campaign failed: \${error.message}\`);
26 }
27 }
28}1import { Res, HttpStatus } from '@nestjs/common';
2import { Response } from 'express';
3
4@Controller('combat')
5export class CombatController {
6
7 @Post('duel')
8 async startDuel(@Body() duelRequest: DuelDto, @Res() response: Response) {
9 const result = await this.combatService.simulateDuel(duelRequest);
10
11 if (result.winner === 'player') {
12 return response.status(HttpStatus.OK).json({
13 message: 'Victory! You won the duel!',
14 reward: result.reward,
15 });
16 } else {
17 return response.status(HttpStatus.ACCEPTED).json({
18 message: 'Defeat... but you fought bravely!',
19 experience: result.experienceGained,
20 });
21 }
22 }
23}1@Controller('inventory')
2export class InventoryController {
3
4 @Get('weapons')
5 getWeapons(@Req() request: Request) {
6 const userAgent = request.headers['user-agent'];
7 const isFromMobileForum = userAgent?.includes('MobileForum');
8
9 const weapons = this.inventoryService.getWeapons();
10
11 if (isFromMobileForum) {
12 // Simplified response for mobile forums
13 return weapons.map(w => ({ name: w.name, damage: w.damage }));
14 }
15
16 return weapons; // Full details for desktop command centers
17 }
18}1import {
2 NotFoundException,
3 BadRequestException,
4 ForbiddenException,
5 ConflictException
6} from '@nestjs/common';
7
8@Controller('tributes')
9export class TributesController {
10
11 @Get(':id')
12 async getTribute(@Param('id') id: string) {
13 const tribute = await this.tributesService.findById(+id);
14
15 if (!tribute) {
16 throw new NotFoundException(\`Tribute with ID \${id} not found in our registries!\`);
17 }
18
19 return tribute;
20 }
21
22 @Post()
23 async claimTribute(@Body() claim: TributeClaimDto) {
24 const existingClaim = await this.tributesService.findClaim(claim.province);
25
26 if (existingClaim) {
27 throw new ConflictException('This tribute has already been claimed by another legion!');
28 }
29
30 if (!claim.legionSize || claim.legionSize < 3) {
31 throw new BadRequestException('You need at least 3 legionaries to claim a tribute!');
32 }
33
34 return this.tributesService.claimTribute(claim);
35 }
36}1@Controller('legion-forces')
2export class LegionForcesController {
3
4 // Wildcard routes
5 @Get('cohort/*/status')
6 getAnyCohortStatus(@Param() params: string[]) {
7 return \`Status of cohort: \${params[0]}\`;
8 }
9
10 // Optional parameters
11 @Get('formation/:type/:cohorts?')
12 getFormation(
13 @Param('type') formationType: string,
14 @Param('cohorts') cohortCount?: string,
15 ) {
16 return {
17 formation: formationType,
18 cohorts: cohortCount ? +cohortCount : 5, // default 5 cohorts
19 };
20 }
21
22 // Route versioning
23 @Version('1')
24 @Get('attack-plan')
25 getAttackPlanV1() {
26 return 'Basic attack plan (v1)';
27 }
28
29 @Version('2')
30 @Get('attack-plan')
31 getAttackPlanV2() {
32 return 'Advanced attack plan with tactical formations (v2)';
33 }
34}Controllers are the first line of contact with the outside world - they must be reliable like experienced centurions who know how to react to every situation during a campaign!
In the next module we will learn about services - specialists who do the real work in the Empire!