Skip to main content

Entities in NestJS with TypeORM

Now that we know how to design a relational database (tables, relationships, foreign keys), it is time to implement it in code. TypeORM is the bridge between our NestJS code and PostgreSQL. This section shows how to translate entity-relationship diagrams into decorated TypeScript classes defining the system structure.

What is TypeORM and Why Do We Need It?

TypeORM is an Object-Relational Mapper (ORM), a tool translating between TypeScript classes (objects) and database tables (relations).

  • Without TypeORM, we would have to write raw SQL queries (CREATE TABLE..., ALTER TABLE...) and handle types manually.
  • With TypeORM, we define Entity classes and decorators. TypeORM automatically creates tables, defines columns, primary keys, constraints, and relationships.

From ER Diagram to Code: How It Translates

Let's take as an example the relationship between USER and ROLE. In our design, a role can belong to multiple users, and a user has a single role (one-to-many relationship).

The following table summarizes how graphical elements in the diagram convert into TypeORM decorators:

In DiagramIn TypeORM (TypeScript)Common Options
Entity (table)@Entity('table_name'){ name: 'users' }
Autoincrement Primary Key@PrimaryGeneratedColumn()'increment', 'uuid'
Attribute / Column@Column(){ unique: true, nullable: false, length: 100 }
1:N Relationship (One-to-Many)@OneToMany(() => Target, (target) => target.property)On principal entity (e.g. Role)
N:1 Relationship (Many-to-One)@ManyToOne(() => Target, (target) => target.property)On entity with FK (e.g. User)
Explicit Foreign Key@JoinColumn({ name: 'col_name' })Defines exact physical FK column name in DB

Practical Setup & Entity Creation Guide

Dependencies Installation

Install the required TypeORM packages and PostgreSQL driver in your NestJS project:

npm install --save @nestjs/typeorm typeorm @nestjs/config pg

Environment Variables Setup (.env)

Create the .env file at the project root. This file serves both for NestJS application configuration and for parameterizing the Docker Compose container:

.env
# Database Configuration
DB_HOST=localhost
DB_PORT=5437
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=nest_db

PostgreSQL Docker Setup (docker-compose.yml)

Create the Docker Compose configuration using PostgreSQL v18 parameterized via .env environment variables:

docker-compose.yml
services:
db:
image: postgres:18
restart: always
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE}
ports:
- '${DB_PORT}:5432'
volumes:
- postgres-data:/var/lib/postgresql/data

volumes:
postgres-data:

To start the database container run:

docker compose up -d

TypeORM Setup in AppModule

Connect NestJS with PostgreSQL dynamically reading variables from .env:

src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // Automatically syncs entity schema in development
}),
}),
],
})
export class AppModule {}

Deep Dive into Decorators and Entity Attributes

Below we explore fundamental options passed to TypeORM decorators:

1. @Entity(name?: string, options?: EntityOptions)

Defines that the class represents a table. If a name is omitted, the lowercase class name is used.

@Entity('roles') // Explicit table name in PostgreSQL

2. @PrimaryGeneratedColumn(strategy?: string)

Sets the primary key for the entity.

  • 'increment': Autoincrementing integer key (default).
  • 'uuid': Universal unique identifier (useful for distributed systems and higher security).

3. @Column(options?: ColumnOptions)

Configures exact column properties in PostgreSQL:

  • type: SQL data type ('varchar', 'int', 'boolean', 'text', 'timestamp', etc.).
  • unique: true forces the value to be unique across the entire table.
  • nullable: true allows null values (NULL). Default is false.
  • default: Sets a default value if none is provided.
  • length: Sets maximum length for varchar.
@Column({ type: 'varchar', length: 150, unique: true, nullable: false })
email: string;

@Column({ type: 'boolean', default: true })
isActive: boolean;

4. Relationships: @OneToMany, @ManyToOne, and @JoinColumn

  • @OneToMany(() => TargetEntity, (instance) => instance.relatedProperty): Defined on the "One" side. Returns an array of child entities.
  • @ManyToOne(() => TargetEntity, (instance) => instance.relatedProperty): Defined on the "Many" side (the entity holding the Foreign Key).
  • @JoinColumn({ name: 'fk_column_name' }): Optional but very useful to specify the exact physical FK column name in DB.
src/roles/entities/role.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { User } from '../../users/entities/user.entity';

@Entity('roles')
export class Role {
@PrimaryGeneratedColumn()
id: number;

@Column({ type: 'varchar', length: 50, unique: true })
name: string;

// A role relates to many users
@OneToMany(() => User, (user) => user.role)
users: User[];
}
src/users/entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Role } from '../../roles/entities/role.entity';

@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;

@Column({ type: 'varchar', length: 120, unique: true })
email: string;

@Column({ type: 'varchar', length: 255 })
passwordHash: string;

// Many users have a single role
@ManyToOne(() => Role, (role) => role.users, { nullable: false })
@JoinColumn({ name: 'role_id' }) // Creates the 'role_id' column as FK in 'users' table
role: Role;
}

Initial Data Seeding (SQL Seed Script)

When starting development, we frequently need to populate the database with base records (e.g. "ADMIN", "USER", and "GUEST" roles, or an initial admin user).

A direct and efficient way to achieve this is creating an SQL script file located outside src (in a dedicated /db folder) and executing it inside the PostgreSQL container via Docker Compose.

Step 1: SQL File Creation (db/seed.sql)

Create the db/ folder at project root and add seed.sql:

db/seed.sql
-- Insert base roles if they don't exist
INSERT INTO roles (name)
VALUES ('ADMIN'), ('USER'), ('GUEST')
ON CONFLICT (name) DO NOTHING;

-- Insert default Admin user linked to ADMIN role
INSERT INTO users (email, "passwordHash", role_id)
VALUES (
'admin@docukelo.edu.co',
'secret_hashed_password',
(SELECT id FROM roles WHERE name = 'ADMIN')
)
ON CONFLICT (email) DO NOTHING;

Step 2: Running Seed via Docker Container

Once entities have been created by TypeORM (with synchronize: true), you can run this file inside the db container in one line using docker compose exec:

docker compose exec -T db psql -U postgres -d nest_db < db/seed.sql
  • docker compose exec -T db: Executes a command inside the database service container.
  • psql -U postgres -d nest_db: Opens PostgreSQL client using postgres user on database nest_db.
  • < db/seed.sql: Redirects local SQL script content as direct input for execution.

Self-Assessment

Cargando cuestionario...