Lesson 1
Integrating a Database
Integrating a Database

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.

Databases

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.

Setting Up TypeORM

Installing TypeORM and Dependencies:

To begin, we need to install TypeORM and the database driver to connect the application to an SQLite database.

Bash
1$ 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.

Configuring TypeORM

Now, we’ll set up TypeORM by configuring it to use SQLite. The configuration is stored in ormconfig.ts. Here's the setup:

TypeScript
1import { 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.

Repository Pattern

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:

TypeScript
1import { 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.

Defining an Entity

Entities in TypeORM represent database tables. In this example, the Todo entity maps to the todos table in the database:

TypeScript
1import { 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 the id 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 like nullable: 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.

Common Column Types and Options

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:

TypeScript
1@Module({ 2 imports: [TypeOrmModule.forFeature([Todo])], 3 providers: [TodoService], 4 controllers: [TodoController], 5}) 6export class TodoModule {}
Data Transfer Object (DTO)

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.

TypeScript
1export 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.

Using the DTO in the Controller

In the TodoController, the CreateTodoDto ensures the data provided when creating a new ToDo matches the expected format:

TypeScript
1@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.

Creating a Service

The service handles interaction with the database through the TypeORM repository. Here's how we use the service to manage our to-do items:

TypeScript
1@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.

Controller

The controller defines the routes that the application will expose. It uses the service methods to interact with the database:

TypeScript
1@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.

Bootstrapping the Application

The final piece is setting up the main application entry point:

TypeScript
1async 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.

Wrapping Up

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!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.