TypeORM Configuration and Entities
In the previous guide, we built a users module using in-memory data storage. However, in a production environment, persistent storage within a relational database is required. TypeORM serves as the bridge between our TypeScript code in NestJS and the PostgreSQL database engine. This guide explains how to translate Entity-Relationship (ER) diagrams into decorated TypeScript entities and configure local database infrastructure using Docker Compose.
1. What is TypeORM and Why Do We Need It?
TypeORM is an Object-Relational Mapper (ORM), a tool that allows developers to interact with relational databases using TypeScript classes and objects rather than writing raw SQL queries manually.
- Without TypeORM: We would need to write raw SQL statements (
CREATE TABLE...,ALTER TABLE...,SELECT * FROM...) and manually handle type mapping and table joins. - With TypeORM: We define decorated Entity classes. TypeORM takes care of generating tables, columns, primary keys, integrity constraints, and foreign key relations automatically or via migrations.
2. From ER Diagram to Code: Schema Translation
Let us examine the relationship between USER and ROLE. In this model, one role can group multiple users, while each user is assigned a single role (a 1:N / N:1 relationship).
The table below summarizes the mapping between relational ER elements and TypeORM decorators:
| In the ER 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: 120 } |
| 1:N Relation (One-to-Many) | @OneToMany(() => Target, (target) => target.property) | On the parent entity (e.g., Role) |
| N:1 Relation (Many-to-One) | @ManyToOne(() => Target, (target) => target.property) | On the entity with the FK (e.g., User) |
| Explicit Foreign Key | @JoinColumn({ name: 'col_name' }) | Specifies physical FK column name in PostgreSQL |
3. Step-by-Step Configuration and Entities Guide
Dependency Installation
Install the required packages for TypeORM, NestJS configuration, and the PostgreSQL database driver (pg):
npm install --save @nestjs/typeorm typeorm @nestjs/config pg
Environment Variable Configuration (.env)
Create a .env file in the project root to centralize connection credentials without committing secrets into source control:
# Database Configuration
DB_HOST=localhost
DB_PORT=5437
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=nest_db
Docker Compose PostgreSQL Setup (docker-compose.yml)
Create docker-compose.yml in the project root to launch a PostgreSQL v18 container, binding external port 5437 to internal port 5432:
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:
Start the container in detached mode:
docker compose up -d
TypeORM Configuration in AppModule
Connect NestJS with PostgreSQL using TypeOrmModule.forRootAsync() and inject ConfigService to dynamically resolve environment variables at runtime:
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 synchronize entity schemas in development
}),
}),
],
})
export class AppModule {}
Entity Decorators and Attributes
The following options configure TypeORM decorators:
1. @Entity(name?: string, options?: EntityOptions)
Defines that the class represents a database table. If no name is provided, the lowercase class name is used.
@Entity('roles') // Explicit table name in PostgreSQL
2. @PrimaryGeneratedColumn(strategy?: string)
Specifies an automatically generated primary key.
'increment': Integer autoincrementing primary key (default).'uuid': Universally unique identifier (ideal for distributed systems and security).
3. @Column(options?: ColumnOptions)
Configures column properties in PostgreSQL:
type: SQL column type ('varchar','int','boolean','text','timestamp', etc.).unique: Whentrue, enforces unique constraints across the table.nullable: Whentrue, permitsNULLvalues. Default isfalse.default: Sets a default fallback value.length: Sets maximum character length for text columns such asvarchar.
@Column({ type: 'varchar', length: 120, unique: true, nullable: false })
email: string;
@Column({ type: 'boolean', default: true })
isActive: boolean;
4. Relationships: @OneToMany, @ManyToOne, and @JoinColumn
@OneToMany(() => Target, (target) => target.property): Placed on the "One" side; returns an array of related child entities.@ManyToOne(() => Target, (target) => target.property): Placed on the "Many" side (the entity storing the foreign key).@JoinColumn({ name: 'col_name' }): Specifies the physical column name of the foreign key in the database table.
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;
// One role can belong 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 belong to a single role
@ManyToOne(() => Role, (role) => role.users, { nullable: false })
@JoinColumn({ name: 'role_id' }) // Physical 'role_id' FK column in 'users' table
role: Role;
}
4. Initial Database Seeding (SQL Seed Script)
When starting development, having base reference data is essential (such as "ADMIN", "USER", and "GUEST" roles, or an initial admin user).
A clean and direct strategy is to maintain an SQL script within a /db directory at the project root and execute it directly inside the PostgreSQL container with Docker Compose.
Creating the SQL File (db/seed.sql)
Create the db/ folder and add seed.sql:
-- Insert base roles if they do not already 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;
Executing the Seed in Docker
Once TypeORM has generated the tables (by running the application with synchronize: true), run the script inside the container 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 thedbservice container without allocating a pseudo-TTY (required for input redirection).psql -U postgres -d nest_db: Launches the PostgreSQL interactive client against the configured database.< db/seed.sql: Pipes the contents of the local file into the standard input ofpsql.