From Raw SQL to Elegant Abstractions with TypeORM
The Object-Relational Impedance Mismatch
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 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.
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.
A Professional ORM for the TypeScript Ecosystem
We are choosing TypeORM because it is:
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 };
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;
From Raw SQL to Elegant Objects
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!
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.
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)
});
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);
The pattern continues, with clean, object-oriented methods.
// 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);
const result = await userRepository.delete(req.params.id);
if (result.affected === 0) { /* Handle not found */ }
res.status(204).send();
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.