We use cookies to enhance your experience on the site
CodeWorlds

Databases and TypeORM - the imperial treasury

Welcome back to the system, young legionary! Consul Caesar.js has a new challenge for you. So far you've learned to build mighty fortifications with NestJS, but every real empire needs a place to store its tributes. It's time to learn treasury management - databases!

What is a database for the empire?

Just as the Romans stored their tributes in secure vaults in the Forum Romanum, applications need somewhere to safely store their data. A database is our digital treasury that:

  • Stores tributes - application data like citizen information, orders, products
  • Protects against loss - ensures durability and integrity of data
  • Enables fast searching - like a treasury register showing exact locations
  • Controls access - only authorized legionaries can reach for tributes

Types of databases - different kinds of vaults

Relational databases (SQL)

These are like organized warehouses with precisely labeled compartments:

1-- Legionaries table
2CREATE TABLE legionaries (
3 id SERIAL PRIMARY KEY,
4 name VARCHAR(100) NOT NULL,
5 cohort VARCHAR(100),
6 tribute_count INTEGER DEFAULT 0
7);
8
9-- Tributes table
10CREATE TABLE tributes (
11 id SERIAL PRIMARY KEY,
12 name VARCHAR(100) NOT NULL,
13 value DECIMAL(10,2),
14 legionary_id INTEGER REFERENCES legionaries(id)
15);

NoSQL databases

These are like flexible bags for tributes, where you can store various things:

1// MongoDB document
2{
3 _id: ObjectId("..."),
4 name: "Marcus Aurelius",
5 cohort: "Legio X Equestris",
6 tributes: [
7 { name: "Golden Coin", value: 100 },
8 { name: "Diamond Ring", value: 500 }
9 ],
10 officers: {
11 optio: "Gaius",
12 signifer: "Marcus"
13 }
14}

TypeORM - the magical treasury management system

TypeORM is a powerful tool that lets us easily manage databases in TypeScript. It's like an imperial treasury management system that automatically translates our requests into a language the database understands.

Installing TypeORM in the application

1npm install @nestjs/typeorm typeorm pg
2npm install --save-dev @types/pg

Configuring the treasury connection

1// app.module.ts
2import { Module } from '@nestjs/common';
3import { TypeOrmModule } from '@nestjs/typeorm';
4
5@Module({
6 imports: [
7 TypeOrmModule.forRoot({
8 type: 'postgres',
9 host: 'localhost',
10 port: 5432,
11 username: 'consul',
12 password: 'imperium123',
13 database: 'roman_empire',
14 entities: [__dirname + '/**/*.entity{.ts,.js}'],
15 synchronize: true, // Only during development!
16 }),
17 ],
18})
19export class AppModule {}

First entity - tribute register

An entity is like a detailed register showing what a specific tribute looks like in our treasury:

1// tribute.entity.ts
2import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
3
4@Entity('tributes')
5export class Tribute {
6 @PrimaryGeneratedColumn()
7 id: number;
8
9 @Column({ length: 100 })
10 name: string;
11
12 @Column('decimal', { precision: 10, scale: 2 })
13 value: number;
14
15 @Column({ nullable: true })
16 description: string;
17
18 @Column({ default: 'unknown' })
19 location: string;
20
21 @CreateDateColumn()
22 collectedAt: Date;
23
24 @Column({ default: false })
25 isCursed: boolean;
26}

TypeORM decorators - instructions for the treasurer

  • @Entity() - marks this as a tribute register (table)
  • @PrimaryGeneratedColumn() - unique tribute number (primary key)
  • @Column() - description of a tribute property (column)
  • @CreateDateColumn() - automatically records the collection date
  • @UpdateDateColumn() - automatically updates the modification date

Basic operations on tributes

Creating a new tribute

1// tribute.service.ts
2import { Injectable } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5import { Tribute } from './tribute.entity';
6
7@Injectable()
8export class TributeService {
9 constructor(
10 @InjectRepository(Tribute)
11 private tributeRepository: Repository<Tribute>,
12 ) {}
13
14 async addTribute(tributeData: Partial<Tribute>): Promise<Tribute> {
15 const newTribute = this.tributeRepository.create(tributeData);
16 return await this.tributeRepository.save(newTribute);
17 }
18
19 async findAllTributes(): Promise<Tribute[]> {
20 return await this.tributeRepository.find();
21 }
22
23 async findTributeById(id: number): Promise<Tribute> {
24 return await this.tributeRepository.findOne({ where: { id } });
25 }
26
27 async updateTribute(id: number, updateData: Partial<Tribute>): Promise<Tribute> {
28 await this.tributeRepository.update(id, updateData);
29 return this.findTributeById(id);
30 }
31
32 async removeTribute(id: number): Promise<void> {
33 await this.tributeRepository.delete(id);
34 }
35}

Column types - different kinds of tributes

1@Entity()
2export class DetailedTribute {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 // Text - tribute name
7 @Column('varchar', { length: 200 })
8 name: string;
9
10 // Integers - quantity
11 @Column('int')
12 quantity: number;
13
14 // Decimals - value
15 @Column('decimal', { precision: 10, scale: 2 })
16 value: number;
17
18 // Booleans - whether cursed
19 @Column('boolean', { default: false })
20 isCursed: boolean;
21
22 // Date - when collected
23 @Column('date')
24 collectionDate: Date;
25
26 // Long text - tribute history
27 @Column('text')
28 history: string;
29
30 // JSON - details
31 @Column('json')
32 details: {
33 material: string;
34 origin: string;
35 previousOwners: string[];
36 };
37}

Data validation - checking tribute quality

1import { IsNotEmpty, IsPositive, IsOptional, Length } from 'class-validator';
2
3export class CreateTributeDto {
4 @IsNotEmpty({ message: 'Every tribute must have a name!' })
5 @Length(3, 100, { message: 'Name must be between 3 and 100 characters' })
6 name: string;
7
8 @IsPositive({ message: 'Tribute value must be greater than zero' })
9 value: number;
10
11 @IsOptional()
12 @Length(0, 500)
13 description?: string;
14
15 @IsOptional()
16 location?: string;
17}

Tribute module configuration

1// tribute.module.ts
2import { Module } from '@nestjs/common';
3import { TypeOrmModule } from '@nestjs/typeorm';
4import { TributeService } from './tribute.service';
5import { TributesController } from './tributes.controller';
6import { Tribute } from './tribute.entity';
7
8@Module({
9 imports: [TypeOrmModule.forFeature([Tribute])],
10 controllers: [TributesController],
11 providers: [TributeService],
12 exports: [TributeService],
13})
14export class TributeModule {}

Resource controller

1// tributes.controller.ts
2import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
3import { TributeService } from './tribute.service';
4import { CreateTributeDto } from './dto/create-tribute.dto';
5
6@Controller('tributes')
7export class TributesController {
8 constructor(private readonly tributeService: TributeService) {}
9
10 @Post()
11 async addTribute(@Body() createTributeDto: CreateTributeDto) {
12 return await this.tributeService.addTribute(createTributeDto);
13 }
14
15 @Get()
16 async getAllTributes() {
17 return await this.tributeService.findAllTributes();
18 }
19
20 @Get(':id')
21 async getTribute(@Param('id') id: string) {
22 return await this.tributeService.findTributeById(+id);
23 }
24
25 @Put(':id')
26 async updateTribute(
27 @Param('id') id: string,
28 @Body() updateData: Partial<CreateTributeDto>
29 ) {
30 return await this.tributeService.updateTribute(+id, updateData);
31 }
32
33 @Delete(':id')
34 async removeTribute(@Param('id') id: string) {
35 await this.tributeService.removeTribute(+id);
36 return { message: 'Tribute has been removed from the treasury!' };
37 }
38}

Error handling - protection against curses

1import { NotFoundException, BadRequestException } from '@nestjs/common';
2
3@Injectable()
4export class TributeService {
5 //
6 async findTributeById(id: number): Promise<Tribute> {
7 if (id <= 0) {
8 throw new BadRequestException('Tribute ID must be greater than zero');
9 }
10
11 const tribute = await this.tributeRepository.findOne({ where: { id } });
12
13 if (!tribute) {
14 throw new NotFoundException(`Tribute with ID ${id} was not found in the treasury`);
15 }
16
17 return tribute;
18 }
19
20 async removeTribute(id: number): Promise<void> {
21 const tribute = await this.findTributeById(id);
22
23 if (tribute.isCursed) {
24 throw new BadRequestException('Cannot remove a cursed tribute!');
25 }
26
27 await this.tributeRepository.delete(id);
28 }
29}

Practical exercise

Now that you know the basics, it's time for your own experiments! Try creating your own entity for legionaries:

1// Your code here
2@Entity('legionaries')
3export class Legionary {
4 // Add properties:
5 // - id (auto-generated)
6 // - name (text, required)
7 // - cohort (text, optional)
8 // - tributeCount (number, default 0)
9 // - joinedAt (join date)
10 // - isActive (whether active, default true)
11}

Summary

Congratulations! You've learned the basics of data treasury management. Now you can:

Configure TypeORM in a NestJS application ✓ Create entities - registers for your tributes ✓ Perform basic CRUD operations on data ✓ Validate data before saving to the treasury ✓ Handle errors and protect against curses

In the next lessons you'll learn advanced techniques like table relationships, migrations, and advanced queries. Consul Caesar.js is proud of your progress!

Go to CodeWorlds