Month 3, Week 3

Introduction to ORMs

From Raw SQL to Elegant Abstractions with TypeORM

Module 1: The Problem ORMs Solve

The Object-Relational Impedance Mismatch

The Two Worlds

As backend architects, we live in two different worlds:

The Object-Oriented World (JavaScript): We think in terms of objects, classes, properties, and methods. Data has behavior.


                        class User {
                            constructor(id, name) { this.id = id; this.name = name; }
                            greet() { console.log(`Hello, ${this.name}`); }
                        }
                    

The Relational World (SQL): We think in terms of tables, rows, and columns. Data is flat and tabular.


                        CREATE TABLE users (id SERIAL, name VARCHAR(100));
                     

The "Impedance Mismatch"

The fundamental difference between these two worlds is the Object-Relational Impedance Mismatch.

Writing the "glue" code to translate between these two worlds is tedious, repetitive, and error-prone. You spend your time manually mapping rows to objects and objects back to SQL statements.


                        // You write this over and over...
                        const { rows } = await db.query('SELECT * FROM users');
                        const users = rows.map(row => {
                            const user = new User();
                            user.id = row.id;
                            user.name = row.username;
                            // ... map every single column ...
                            return user;
                        });
                    

This is boilerplate that distracts from our real job: building business logic.

The Solution: ORMs

Object-Relational Mappers are libraries that act as a translator between your object-oriented code and your relational database.

An ORM lets you work with your database using the language you already know (JavaScript/TypeScript), instead of writing SQL strings.

You define the mapping once, and the ORM handles the translation for you.

Module 2: Introduction to TypeORM

A Professional ORM for the TypeScript Ecosystem

Why TypeORM?

We are choosing TypeORM because it is:

  • Mature and Feature-Rich: It's a powerful, well-established ORM.
  • Built for TypeScript: It leverages TypeScript's features to provide excellent type safety and autocompletion.
  • Database Agnostic: The code you write can work with PostgreSQL, MySQL, SQL Server, and more.
  • The Foundation of NestJS: Learning it now is a direct prerequisite for mastering the NestJS framework.

Core Concepts of TypeORM

  1. Data Source: The object that holds your database connection configuration.
  2. Entity: A class that maps to a database table. You use decorators to define the mapping.
  3. Repository: An object that provides a rich API for interacting with a specific table (e.g., `find`, `save`, `remove`).

Installation and Setup

We'll install TypeORM and the PostgreSQL driver.


                        # We also install 'reflect-metadata', a required dependency
                        npm install typeorm reflect-metadata pg
                    

Then, we create a data source to configure our connection.


                        // db/data-source.js
                        require('reflect-metadata');
                        const { DataSource } = require('typeorm');

                        const AppDataSource = new DataSource({
                            type: 'postgres',
                            host: 'localhost',
                            port: 5432,
                            username: 'your_user',
                            password: 'your_password',
                            database: 'your_db',
                            synchronize: true, // DEV ONLY: automatically creates schema
                            logging: true,
                            entities: [/* ... paths to your entity files ... */],
                        });

                        module.exports = { AppDataSource };
                     

Defining an Entity

An entity is a class that represents a database table. We use decorators (`@`) to add metadata.

`entities/User.js`


                        const { Entity, PrimaryGeneratedColumn, Column } = require('typeorm');

                        @Entity('users') // Maps this class to the 'users' table
                        class User {
                            @PrimaryGeneratedColumn()
                            id;

                            @Column({ type: 'varchar', length: 50, unique: true })
                            username;
                            
                            @Column({ type: 'varchar', unique: true })
                            email;

                            @Column({ name: 'password_hash' })
                            passwordHash;
                        }

                        module.exports = User;
                    

Mid-Lecture Knowledge Check

Module 3: Refactoring the API with TypeORM

From Raw SQL to Elegant Objects

Connecting and Getting a Repository

In our main `app.js`, we initialize the data source. Then, in our controllers, we get the repository for the entity we want to work with.


                        // In userController.js
                        const { AppDataSource } = require('../db/data-source');
                        const User = require('../entities/User');

                        const userRepository = AppDataSource.getRepository(User);
                        
                        // Now we can use `userRepository` to query the database!
                    

Read (GET) - Before and After

Before (Raw SQL):


                        const { rows } = await db.query('SELECT * FROM users');
                        res.json(rows);
                    

After (TypeORM):


                        const users = await userRepository.find();
                        res.json(users);
                    

Notice how we are now working with methods, not SQL strings.

Read One (GET /:id) - Before and After

Before (Raw SQL):


                        const { rows } = await db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
                        const user = rows[0];
                     

After (TypeORM):


                        const user = await userRepository.findOneBy({ 
                            id: parseInt(req.params.id) 
                        });
                      

Create (POST) - Before and After

Before (Raw SQL):


                        const text = 'INSERT INTO users(username, email) VALUES($1, $2) RETURNING *';
                        const values = [req.body.username, req.body.email];
                        const { rows } = await db.query(text, values);
                    

After (TypeORM):


                        // 1. Create a new instance of our Entity
                        const newUser = userRepository.create(req.body);

                        // 2. Save it to the database
                        const savedUser = await userRepository.save(newUser);
                        res.status(201).json(savedUser);
                    

Update (PATCH) & Delete (DELETE)

The pattern continues, with clean, object-oriented methods.

Update


                        // First, find the user
                        const user = await userRepository.findOneBy({ id: ... });
                        // Then, merge changes and save
                        userRepository.merge(user, req.body);
                        const updatedUser = await userRepository.save(user);
                    

Delete


                        const result = await userRepository.delete(req.params.id);
                        if (result.affected === 0) { /* Handle not found */ }
                        res.status(204).send();
                    

In-Class Practical Exercise

Refactoring the `createUser` Endpoint

You will be given the Express API project with TypeORM and a `User` entity already configured. Your task is to refactor just the `createUser` controller function to use the ORM instead of raw SQL.

  1. Set up your project and run `npm install`.
  2. Initialize the `DataSource` in `app.js`.
  3. In `userController.js`, get the repository for the `User` entity.
  4. In the `createUser` function:
    • Remove the `bcrypt` hashing and the raw SQL `INSERT` statement.
    • Use `userRepository.create(req.body)` to create a new user entity instance from the request body.
    • Use `await userRepository.save(newUser)` to persist it to the database.
    • Send the result of the `save` operation back to the client with a `201` status.
  5. Test your refactored endpoint using Postman to create a new user.

Final Knowledge Check