I’ve written about setting up a TypeORM DataSource and seeding a database before, but I kept getting the same question in my inbox — “okay but how do I actually use this thing for basic operations?” Fair. So here’s the part that actually matters day to day: create, read, update, delete. The stuff you’ll type a hundred times before lunch.
I’ll be honest, when I first moved from raw SQL queries to TypeORM, I overcomplicated this. I was writing query builders for things a simple repository method could handle in one line. Don’t do that. Save the query builder for when you actually need it — joins, complex filters, pagination logic. For plain CRUD, the repository pattern is faster to write and easier to read six months later when you’ve forgotten what your own code does.
The Entity We’re Working With
Let’s keep this grounded with something real — a Task entity, since almost every app ends up needing some version of this.
typescript
// entities/Task.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";
@Entity()
export class Task {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column({ default: false })
completed: boolean;
@CreateDateColumn()
createdAt: Date;
}
Nothing fancy. If you followed my DataSource setup post, this entity should already be registered in your entities array and ready to sync or migrate.
Create
typescript
import { AppDataSource } from "../data-source";
import { Task } from "../entities/Task";
const taskRepository = AppDataSource.getRepository(Task);
export const createTask = async (title: string) => {
const task = taskRepository.create({ title });
return await taskRepository.save(task);
};
Quick note here — create() doesn’t hit the database. It just builds the entity instance in memory. save() is what actually writes it. I mixed these up constantly when I started, and spent an embarrassing amount of time debugging why my “saved” records weren’t showing up anywhere.
Read
For a single record:
typescript
export const getTaskById = async (id: number) => {
return await taskRepository.findOneBy({ id });
};
For all records:
typescript
export const getAllTasks = async () => {
return await taskRepository.find();
};
If you need filtering, find() accepts a where clause too:
typescript
export const getIncompleteTasks = async () => {
return await taskRepository.find({ where: { completed: false } });
};
This is usually enough. I only reach for createQueryBuilder once filters start stacking up or I need joins across relations.
Update
typescript
export const updateTask = async (id: number, updates: Partial<Task>) => {
await taskRepository.update(id, updates);
return await taskRepository.findOneBy({ id });
};
One thing that tripped me up early on — update() doesn’t return the updated entity, it returns an UpdateResult with metadata about the operation. If you want the fresh record back (which, let’s be real, you usually do), you need that second query. Annoying at first, makes sense once you get why: it’s optimized to avoid an unnecessary round trip if you don’t need the data back.
Delete
typescript
export const deleteTask = async (id: number) => {
const result = await taskRepository.delete(id);
return result.affected !== 0;
};
I always check result.affected before telling the frontend “deleted successfully.” Otherwise you end up with a UI confidently confirming deletion of something that was never there in the first place — happened to me during a demo, not fun.
Wrapping It in a Route
None of this matters until it’s wired to actual endpoints. Here’s a basic Express setup:
typescript
import { Router } from "express";
import { createTask, getAllTasks, getTaskById, updateTask, deleteTask } from "./task.service";
const router = Router();
router.post("/tasks", async (req, res) => {
const task = await createTask(req.body.title);
res.status(201).json(task);
});
router.get("/tasks", async (req, res) => {
res.json(await getAllTasks());
});
router.get("/tasks/:id", async (req, res) => {
const task = await getTaskById(Number(req.params.id));
task ? res.json(task) : res.status(404).send("Task not found");
});
router.put("/tasks/:id", async (req, res) => {
const task = await updateTask(Number(req.params.id), req.body);
res.json(task);
});
router.delete("/tasks/:id", async (req, res) => {
const success = await deleteTask(Number(req.params.id));
success ? res.status(204).send() : res.status(404).send("Task not found");
});
export default router;
A Mistake Worth Mentioning
Early on, I skipped error handling entirely in these service functions, figuring I’d “add it later.” Later never came until a malformed request crashed a production endpoint at 11pm on a Friday. Wrap your database calls in try/catch at the route level at minimum, and if you’re passing user input straight into update() or create(), validate it first. TypeORM won’t stop you from saving garbage data — it just does what you tell it.
If you’ve already gone through my post on seeding your database, you’ll have sample data ready to test all of this against without manually creating records every time you restart your server. And if you haven’t set up your DataSource yet, that’s genuinely the place to start before any of this will run.
What’s tripping you up right now — relations between entities, or something with migrations? Let me know and I’ll cover it next.
