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 Diagram | In 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:
# 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:
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:
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:trueforces the value to be unique across the entire table.nullable:trueallows null values (NULL). Default isfalse.default: Sets a default value if none is provided.length: Sets maximum length forvarchar.
@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.
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[];
}
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:
-- 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 usingpostgresuser on databasenest_db.< db/seed.sql: Redirects local SQL script content as direct input for execution.