In this lesson, we’ll enhance your NestJS application by integrating a database. This will allow your application to store and manage data persistently.
We’ll focus on setting up TypeORM for an SQLite database and implementing basic CRUD operations using the database. Let's see how the TodoController, TodoService, and TypeORM repository work together to handle and persist data.
NestJS is database agnostic, meaning it can integrate easily with both SQL and NoSQL databases. To connect to a database, we load the appropriate Node.js
driver. For convenience, NestJS offers integration with TypeORM, Sequelize, and Mongoose. In this lesson, we’ll work with TypeORM, as it provides key features like repository injection and asynchronous configuration, making it easier to manage your database interactions.
Installing TypeORM and Dependencies:
To begin, we need to install TypeORM and the database driver to connect the application to an SQLite database.
Bash1$ npm install --save @nestjs/typeorm typeorm sqlite3
This command installs TypeORM, NestJS integration, and the SQLite driver. In the CodeSignal environment, this step is already done for you.
Now, we’ll set up TypeORM by configuring it to use SQLite. The configuration is stored in ormconfig.ts
. Here's the setup:
TypeScript1import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 3export const ormConfig: TypeOrmModuleOptions = { 4 type: 'sqlite', 5 database: 'database.sqlite', 6 entities: [__dirname + '/../**/*.entity{.ts,.js}'], 7 synchronize: true, 8};
This configuration file tells TypeORM to use SQLite as the database. The synchronize: true
option automatically syncs the database schema with your entity definitions during development, making it easier to manage changes.
The entities
option specifies which classes should be mapped to tables in the database. In TypeORM, entities define the structure of the data (columns, types, relationships). Each entity represents a table, and TypeORM will automatically handle creating or updating these tables based on your entity definitions.
TypeORM follows the Repository design pattern. Each entity in your application corresponds to a repository that provides access to database operations for that entity. These repositories manage operations like creating, updating, and deleting records.
Here’s how we integrate TypeORM in the AppModule
:
TypeScript1import { Module } from '@nestjs/common'; 2import { TypeOrmModule } from '@nestjs/typeorm'; 3import { ormConfig } from './typeorm/ormconfig'; 4import { TodoModule } from './todo/todo.module'; 5 6@Module({ 7 imports: [ 8 TypeOrmModule.forRoot(ormConfig), 9 TodoModule, 10 ], 11}) 12export class AppModule {}
In this module, TypeOrmModule.forRoot(ormConfig)
initializes the database connection using the configuration we specified earlier. The TodoModule
handles all the to-do-related logic.
Entities in TypeORM represent database tables. In this example, the Todo
entity maps to the todos
table in the database:
TypeScript1import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 3@Entity() 4export class Todo { 5 @PrimaryGeneratedColumn() 6 id: number; 7 8 @Column() 9 title: string; 10 11 @Column({ nullable: true }) 12 description: string; 13}
In the code above, we use several decorators:
@Entity()
: Marks the class as a database entity, meaning TypeORM will create a corresponding table in the database for this class.@PrimaryGeneratedColumn()
: Marks theid
field as the primary key, and TypeORM will automatically generate values for this column.@Column()
: Marks fields as columns in the database. We can add options likenullable: true
to indicate that a column can be empty.
These decorators are essential for defining the structure of database tables and specifying which fields should be treated as columns. They allow you to directly map your TypeScript classes to database tables and columns, making it easier to work with the data in your application.
TypeORM supports all of the most commonly used database-supported column types. Here are some examples:
- Common types:
int
(integer)varchar
(variable-length string)date
(date without time)
- Common options:
nullable: boolean
: Specifies whether the column can have a null value.name: string
: Specifies a custom name for the column in the database.unique: boolean
: Specifies if the value in this column should be unique across the table.
To ensure TypeORM recognizes this entity, we register it in the TodoModule
:
TypeScript1@Module({ 2 imports: [TypeOrmModule.forFeature([Todo])], 3 providers: [TodoService], 4 controllers: [TodoController], 5}) 6export class TodoModule {}
A Data Transfer Object (DTO) defines the structure of the data that is being transferred, ensuring type safety and consistency. In our ToDo application, we use a CreateTodoDto
class to represent the data needed to create a new ToDo item. This class includes a mandatory title
field and an optional description
.
TypeScript1export class CreateTodoDto { 2 title: string; 3 description?: string; 4}
The CreateTodoDto
defines the expected shape of data for creating a new ToDo item, making sure that each request is consistent and predictable. This also helps with validation and ensures the code remains clear and easy to maintain.
In the TodoController, the CreateTodoDto
ensures the data provided when creating a new ToDo matches the expected format:
TypeScript1@Post() 2@Render('index') 3async create(@Body() createTodoDto: CreateTodoDto) { 4 const message = await this.todoService.create(createTodoDto); 5 return { message }; 6}
Using DTOs helps maintain type safety, simplifies validation, and improves code readability by making the expected data structure explicit.
The service handles interaction with the database through the TypeORM repository. Here's how we use the service to manage our to-do items:
TypeScript1@Injectable() 2export class TodoService { 3 constructor( 4 @InjectRepository(Todo) 5 private readonly todoRepository: Repository<Todo>, 6 ) { 7 this.logDatabaseIntegration(); 8 } 9 10 async findAll(): Promise<Todo[]> { 11 console.log('Fetching all Todos from the database...'); 12 return this.todoRepository.find(); 13 } 14 15 async create(createTodoDto: CreateTodoDto): Promise<string> { 16 const existingTodo = await this.todoRepository.findOne({ where: { title: createTodoDto.title } }); 17 18 if (existingTodo) { 19 return `A ToDo item with the title "${createTodoDto.title}" already exists.`; 20 } 21 22 const todo = this.todoRepository.create(createTodoDto); 23 await this.todoRepository.save(todo); 24 console.log(`Todo created with title: ${createTodoDto.title}`); 25 return 'ToDo created successfully!'; 26 } 27 28 private logDatabaseIntegration() { 29 console.log('Database integration has been successfully established.'); 30 } 31}
The TodoService
class uses @InjectRepository(Todo)
to inject the Todo
repository, allowing access to database operations. The create
method checks if a ToDo with the same title already exists before creating a new one. this.todoRepository.create(createTodoDto)
creates a new Todo
entity, and this.todoRepository.save(todo)
stores it in the database.
The findAll
method retrieves all ToDo items from the database, while the logDatabaseIntegration()
method confirms that the database is set up correctly.
The controller defines the routes that the application will expose. It uses the service methods to interact with the database:
TypeScript1@Controller('todos') 2export class TodoController { 3 constructor(private readonly todoService: TodoService) {} 4 5 @Get() 6 @Render('index') 7 async findAll() { 8 const todos = await this.todoService.findAll(); 9 return { title: 'ToDo List', todos }; 10 } 11 12 @Post() 13 @Render('index') 14 async create(@Body() createTodoDto: CreateTodoDto) { 15 const message = await this.todoService.create(createTodoDto); 16 return { message }; 17 } 18}
The TodoController
class uses decorators to define HTTP methods and routes. The @Get()
method retrieves all ToDo items and renders them on the index view. The @Post()
method creates a new ToDo using the data provided by the user.
The final piece is setting up the main application entry point:
TypeScript1async function bootstrap() { 2 const app = await NestFactory.create<NestExpressApplication>(AppModule); 3 app.setBaseViewsDir(join(__dirname, '..', 'views')); 4 app.setViewEngine('hbs'); 5 console.log('Starting the NestJS application...'); 6 await app.listen(3000); 7 console.log('NestJS application is running on http://localhost:3000'); 8} 9bootstrap();
This function starts the application, making it accessible on port 3000, with Handlebars as the view engine.
In this lesson, you’ve integrated a database into your NestJS application using TypeORM to handle persistent data storage. By interacting with repositories, controllers, and services, your application is now fully capable of performing CRUD operations on the ToDo items.
Once the setup is complete, you can test the application by creating, viewing, and managing ToDo items through the UI and observing how changes persist in the SQLite database.
Let’s move on to the practice section and apply what you’ve learned!