I’ll be honest, the first time I set up TypeORM on a real project, I copied a config from a Medium article, ran npm start, saw it connect, and called it done. Three weeks later, in production, the app silently dropped a table column because synchronize: true was still on. That one decision cost me a Saturday and a fairly uncomfortable Slack message from a teammate.
So this post isn’t the “five minute quickstart” version. It’s the configuration setup I actually use now, after getting burned once.
What you need before starting
You need Node.js installed, a database (I’ll use PostgreSQL here since that’s what most of my projects run, but the same logic applies to MySQL or SQLite), and a basic TypeScript project. If you’re starting fresh:
bash
npm init -y
npm install typeorm reflect-metadata pg
npm install -D typescript ts-node @types/node
The pg package is the PostgreSQL driver. Swap it for mysql2 or sqlite3 if you’re using something else, the rest of the config barely changes.
You also need a tsconfig.json with two settings that trip people up constantly:
json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Skip these and your entity decorators just won’t work. You’ll get cryptic errors about metadata that have nothing to do with what you actually did wrong.
The DataSource setup
Modern TypeORM (0.3 and up) moved away from the old createConnection approach to a DataSource class. If you’re following an older tutorial that uses createConnection, that’s outdated, and it’ll bite you with version mismatches.
typescript
// data-source.ts
import "reflect-metadata";
import { DataSource } from "typeorm";
export const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432"),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
synchronize: false,
logging: process.env.NODE_ENV === "development",
entities: ["src/entities/**/*.ts"],
migrations: ["src/migrations/**/*.ts"],
});
A few things here that matter more than they look like they do.
synchronize: false. I set this to true once, on a project that already had production data, and TypeORM altered my schema based on entity changes without asking. It’s fine for a throwaway prototype where you don’t care about the data. The moment real data exists, turn it off and use migrations instead. Yes, migrations take more setup time. They also won’t quietly drop your users table at 2am.
Environment variables, not hardcoded credentials. I know this sounds like the obvious advice everyone gives, but I still see config files in client repos with a real database password sitting in plain text because “it’s just for local dev.” Use a .env file with the dotenv package, and make sure .env is in your .gitignore before your first commit, not after.
Initializing the connection
Where you initialize AppDataSource matters more than people think. Don’t scatter .initialize() calls across multiple files, you’ll get race conditions where parts of your app try to query before the connection is ready.
typescript
// index.ts
import { AppDataSource } from "./data-source";
AppDataSource.initialize()
.then(() => {
console.log("Database connected");
// start your server here, not before
})
.catch((error) => {
console.error("Database connection failed:", error);
process.exit(1);
});
That process.exit(1) on failure is intentional. An app that starts and serves traffic without a working database connection is worse than one that crashes immediately, because at least the crash tells you something’s wrong.
Connection pooling, the setting nobody mentions
This is the part most tutorials skip entirely, and it’s the one that actually matters once you have real traffic. By default, TypeORM’s PostgreSQL driver uses node-postgres’s pool with fairly conservative defaults. If you’re deploying to something like Heroku or a small VPS, you’ll want to set this explicitly:
typescript
export const AppDataSource = new DataSource({
// ...previous config
extra: {
max: 10,
connectionTimeoutMillis: 5000,
},
});
max: 10 caps the pool at 10 connections. If your database plan only allows 20 total connections and you’re running multiple app instances, you’ll exhaust them fast without realizing it until you start seeing timeout errors that make no sense at first glance. I learned this after a deploy where two instances of the same app were fighting over a 20-connection limit.
A basic entity to go with it
Since config without an entity is a bit abstract, here’s a minimal one to test the setup:
typescript
// entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ unique: true })
email: string;
@Column()
name: string;
@CreateDateColumn()
createdAt: Date;
}
I default to UUID primary keys over auto-increment integers now, mostly because it saves a headache later if you ever need to merge data from multiple environments or expose IDs in a public API without leaking row counts.
Running your first migration
With synchronize off, you generate migrations instead:
bash
npx typeorm migration:generate src/migrations/InitialMigration -d src/data-source.ts
npx typeorm migration:run -d src/data-source.ts
The generate command compares your entities against the current database state and writes the SQL needed to catch up. Always read the generated file before running it. TypeORM is good, not psychic, and it occasionally generates a column rename as a drop-and-add, which means data loss if you don’t catch it.
Where this usually goes wrong
If I had to bet on what breaks for someone following this guide, it’s one of three things: forgetting the decorator settings in tsconfig.json, leaving synchronize: true on past the prototype stage, or not setting connection pool limits before deploying somewhere with a connection cap. None of these throw an obvious error pointing you to the fix, which is exactly why they’re worth getting right upfront instead of debugging them under pressure later.
