LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
Lesson 1226 min read

Databases with TypeORM

Connect NestJS to a SQL database with TypeORM: configure the connection, define entities, inject repositories, build a real CRUD service and model relations.

On this page 14 sections

Introduction

Until now, data lived in arrays that vanished every time the server restarted. Real applications persist data in a database. NestJS is database-agnostic, but its official @nestjs/typeorm integration is one of the most common choices, and works with MySQL, PostgreSQL, SQLite, Microsoft SQL Server and more.

What You Will Learn
  • What an ORM is and why it is useful.
  • How to connect Nest to MySQL using TypeOrmModule and ConfigService.
  • How to define tables as entity classes.
  • How to inject repositories and build a real CRUD service.
  • How to model relations and why migrations matter.

What is an ORM?

An ORM (Object-Relational Mapper) maps database tables to classes and rows to objects. Instead of writing SELECT * FROM products WHERE id = 1, you call productsRepository.findOneBy({ id: 1 }) and get back a typed Product object. TypeORM is written in TypeScript and uses decorators — a natural fit for Nest.

Still Learn SQL

An ORM writes SQL for you, but understanding the SQL it generates is essential for debugging and performance. Our MySQL course is a great companion to this lesson.

Installing TypeORM

npm install @nestjs/typeorm typeorm mysql2
# for PostgreSQL use: npm install @nestjs/typeorm typeorm pg

Connecting to the Database

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductsModule } from './products/products.module.js';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'mysql',
host: config.get<string>('DATABASE_HOST'),
port: Number(config.get('DATABASE_PORT', 3306)),
username: config.get<string>('DATABASE_USER'),
password: config.get<string>('DATABASE_PASSWORD'),
database: config.get<string>('DATABASE_NAME'),
autoLoadEntities: true,
synchronize: config.get('NODE_ENV') !== 'production',
}),
}),
ProductsModule,
],
})
export class AppModule {}

autoLoadEntities: true automatically registers every entity used in a TypeOrmModule.forFeature() call, so you do not need to list them all here.

synchronize Is for Development Only

synchronize: true makes TypeORM alter your tables to match your entities on every startup. That is convenient while learning, but in production it can silently drop columns and lose data. Use migrations in production.

Defining an Entity

// src/products/entities/product.entity.ts
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('products')
export class Product {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 120 })
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@Column({ default: true })
inStock: boolean;
@CreateDateColumn()
createdAt: Date;
}

Each decorated property becomes a column. @PrimaryGeneratedColumn() creates an auto-incrementing id, and @CreateDateColumn() is filled in automatically when a row is inserted.

Decimal Columns Come Back as Strings

With MySQL and PostgreSQL drivers, decimal columns are returned as strings (e.g. "55000.00") to avoid losing precision. Add a column transformer or convert them in your service if you need real numbers.

Repositories in a Feature Module

// src/products/products.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from './entities/product.entity.js';
import { ProductsController } from './products.controller.js';
import { ProductsService } from './products.service.js';
@Module({
imports: [TypeOrmModule.forFeature([Product])],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}

forFeature([Product]) registers a Repository<Product> provider in this module, which you inject with @InjectRepository().

A Database-Backed CRUD Service

// src/products/products.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateProductDto } from './dto/create-product.dto.js';
import { UpdateProductDto } from './dto/update-product.dto.js';
import { Product } from './entities/product.entity.js';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productsRepository: Repository<Product>,
) {}
create(dto: CreateProductDto): Promise<Product> {
const product = this.productsRepository.create(dto);
return this.productsRepository.save(product);
}
findAll(): Promise<Product[]> {
return this.productsRepository.find({ order: { createdAt: 'DESC' } });
}
async findOne(id: number): Promise<Product> {
const product = await this.productsRepository.findOneBy({ id });
if (!product) {
throw new NotFoundException(`Product ${id} not found`);
}
return product;
}
async update(id: number, dto: UpdateProductDto): Promise<Product> {
const product = await this.findOne(id);
Object.assign(product, dto);
return this.productsRepository.save(product);
}
async remove(id: number): Promise<void> {
const product = await this.findOne(id);
await this.productsRepository.remove(product);
}
}

Because the controller from lesson 4 only ever called service methods, it works unchanged — this is dependency injection and thin controllers paying off.

Repository MethodPurpose
create(dto)Builds an entity instance in memory (does not save).
save(entity)Inserts a new row or updates an existing one.
find(options)Returns many rows, with optional where, order, relations, skip/take.
findOneBy(where)Returns one row or null.
remove(entity) / delete(criteria)Deletes rows.
count(options)Counts matching rows.

Relations

Most data is related — a category has many products, and each product belongs to one category:

// category.entity.ts
@Entity('categories')
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@OneToMany(() => Product, (product) => product.category)
products: Product[];
}
// product.entity.ts (add to the Product class)
@ManyToOne(() => Category, (category) => category.products, { onDelete: 'SET NULL' })
category: Category;
// load products together with their category
this.productsRepository.find({ relations: { category: true } });

Migrations

Migrations are versioned scripts that change the database schema step by step (add a column, create a table). They are generated from your entity changes, committed to Git, reviewed like code, and run during deployment — giving you a safe, repeatable history of every schema change. TypeORM's CLI generates and runs them; set synchronize to false in production and rely on migrations instead.

Alternatives to TypeORM

Prisma (schema-first, with a generated, fully typed client), MikroORM, Sequelize and Mongoose (for MongoDB, via @nestjs/mongoose) are all popular in Nest projects. The module/provider/DI patterns you learned stay exactly the same.

Common Mistakes

Avoid These Mistakes
  • Leaving synchronize: true enabled in production.
  • Forgetting TypeOrmModule.forFeature([Entity]) in the feature module, causing "can't resolve ProductRepository" errors.
  • Hard-coding database credentials instead of reading them from ConfigService.
  • Loading every relation on every query, creating slow, oversized responses.

Best Practices

  • Configure the connection with forRootAsync() and ConfigService.
  • Keep entities inside their feature folder and use autoLoadEntities.
  • Use migrations for every schema change outside local development.
  • Paginate list endpoints with skip and take instead of returning whole tables.
  • Throw NotFoundException when findOneBy() returns null.

Frequently Asked Questions

Both are excellent. TypeORM uses decorated entity classes (very "Nest-like"); Prisma uses a separate schema file and a generated client with outstanding type safety. Learn one well — the concepts transfer.

Yes. Use repository.query('SELECT ...', [params]) or the QueryBuilder for complex queries. Always use parameters, never string concatenation, to avoid SQL injection.

Inject DataSource and call dataSource.transaction(async (manager) => { ... }); every operation through that manager runs in one transaction.

Key Takeaways

  • @nestjs/typeorm integrates TypeORM with Nest's module and DI system.
  • TypeOrmModule.forRootAsync() configures the connection from ConfigService.
  • Entities are classes decorated with @Entity() and @Column().
  • forFeature() registers repositories, injected with @InjectRepository().
  • Use synchronize only in development; use migrations in production.

Summary

Your API now stores data in a real database, with entities, repositories and relations. Next, you will make sure it keeps working as it grows, by writing unit and end-to-end tests.

Next Lesson →

Testing NestJS Applications