Home Database TypeORM Basics: Entities, Repositories & Queries Explained

TypeORM Basics: Entities, Repositories & Queries Explained

In Plain English 🔥
Imagine your database is a giant filing cabinet with thousands of folders. Writing raw SQL is like climbing into that cabinet yourself, reading every label by hand, and reorganising folders one at a time. TypeORM is like hiring a brilliant personal assistant who speaks both your language (TypeScript/JavaScript) and the filing cabinet's language (SQL) — you just tell them what you want in plain terms, and they handle all the cabinet-diving for you. Your code stays clean, your database stays organised, and you never have to write 'SELECT * FROM users WHERE id = 1' again unless you really want to.
⚡ Quick Answer
Imagine your database is a giant filing cabinet with thousands of folders. Writing raw SQL is like climbing into that cabinet yourself, reading every label by hand, and reorganising folders one at a time. TypeORM is like hiring a brilliant personal assistant who speaks both your language (TypeScript/JavaScript) and the filing cabinet's language (SQL) — you just tell them what you want in plain terms, and they handle all the cabinet-diving for you. Your code stays clean, your database stays organised, and you never have to write 'SELECT * FROM users WHERE id = 1' again unless you really want to.

Every production application eventually needs to talk to a database. For most Node.js and TypeScript projects, that conversation starts simply — a raw query here, a connection pool there — and quickly turns into a maintenance nightmare of scattered SQL strings, manual column mapping, and zero type safety. One renamed column in your database and suddenly half your application silently breaks at runtime, not compile time. This is exactly why Object-Relational Mappers exist, and TypeORM is one of the most battle-tested in the TypeScript ecosystem.

TypeORM solves three expensive problems at once. First, it maps your database tables to TypeScript classes, so renaming a column becomes a compiler error instead of a 3am incident. Second, it abstracts away database dialects — the same code runs against PostgreSQL, MySQL, SQLite, or MariaDB with minimal config changes. Third, it provides a rich query builder that keeps complex queries readable and composable, without sacrificing performance or flexibility.

By the end of this article you'll be able to define entities with decorators, wire up a DataSource, run CRUD operations using repositories, build relationships between tables, and write type-safe queries with the QueryBuilder. You'll also know the real-world gotchas that trip up most developers during their first week with TypeORM — the ones the official docs bury in footnotes.

Setting Up TypeORM: DataSource, Entities and Your First Table

Before you write a single query, TypeORM needs to know two things: where your database is, and what your data looks like. These two concerns map directly to its two core primitives — the DataSource and the Entity.

A DataSource is your application's single connection contract with the database. It holds credentials, connection pool settings, and crucially, a list of every Entity class TypeORM should manage. Think of it as the contract you sign with your filing assistant before they start work.

An Entity is a TypeScript class decorated with @Entity(). Each property decorated with @Column() becomes a column in the corresponding table. TypeORM reads these decorators at runtime and uses them to generate — or validate — your schema. The @PrimaryGeneratedColumn() decorator marks the auto-incrementing primary key, so you never have to think about ID generation again.

The magic here is bidirectionality: your TypeScript class IS your schema documentation. A new developer joins the team, reads your entity file, and immediately understands exactly what the table looks like — no need to open a database GUI or read a migration file.

Run synchronize: true only in development. In production, always use migrations. We'll cover why in the Gotchas section.

UserEntity.ts · TYPESCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
// ─── 1. Install dependencies first ───────────────────────────────────────────
// npm install typeorm reflect-metadata pg
// npm install -D @types/node typescript
// Also add to tsconfig.json:
//   "experimentalDecorators": true,
//   "emitDecoratorMetadata": true

import "reflect-metadata"; // MUST be imported once, before anything else
import {
  DataSource,
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
} from "typeorm";

// ─── 2. Define the Entity (maps to a 'users' table) ──────────────────────────
@Entity("users") // explicit table name — avoids surprises if class is renamed
export class User {
  @PrimaryGeneratedColumn()
  id: number; // TypeORM handles auto-increment — never set this manually

  @Column({ length: 100 })
  fullName: string; // maps to column 'full_name' by default (snake_case conversion)

  @Column({ unique: true })
  email: string; // unique constraint applied at the DB level

  @Column({ default: true })
  isActive: boolean; // new users are active by default

  @CreateDateColumn()
  createdAt: Date; // TypeORM sets this automatically on INSERT — don't touch it
}

// ─── 3. Create and initialise the DataSource ─────────────────────────────────
export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "forge_user",
  password: "forge_pass",
  database: "forge_db",
  synchronize: true,   // DEV ONLY: auto-creates tables. Use migrations in prod.
  logging: true,       // prints generated SQL — great for learning what TypeORM does
  entities: [User],    // every entity the DataSource manages must be listed here
});

// ─── 4. Bootstrap — initialise the connection before doing anything else ─────
async function bootstrap() {
  await AppDataSource.initialize(); // opens the connection pool
  console.log("DataSource initialised — connected to PostgreSQL");

  // Get the repository for User — this is your gateway to the 'users' table
  const userRepository = AppDataSource.getRepository(User);

  // Create a new user entity instance
  const newUser = userRepository.create({
    fullName: "Ada Lovelace",
    email: "ada@codeforge.io",
    isActive: true,
  });

  // .save() performs an INSERT (or UPDATE if the entity has an id)
  const savedUser = await userRepository.save(newUser);
  console.log("User saved:", savedUser);

  // Find all active users
  const activeUsers = await userRepository.find({
    where: { isActive: true },
    order: { createdAt: "DESC" }, // most recently created first
  });
  console.log(`Found ${activeUsers.length} active user(s):", activeUsers);

  await AppDataSource.destroy(); // clean up — close the connection pool
}

bootstrap().catch(console.error);
▶ Output
query: SELECT * FROM "users" WHERE 1=1
query: CREATE TABLE "users" ("id" SERIAL NOT NULL, "full_name" character varying(100) NOT NULL, "email" character varying NOT NULL UNIQUE, "is_active" boolean NOT NULL DEFAULT true, "created_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_users" PRIMARY KEY ("id"))
DataSource initialised — connected to PostgreSQL
query: INSERT INTO "users"("full_name", "email", "is_active") VALUES ($1, $2, $3) RETURNING "id", "created_at"
User saved: { id: 1, fullName: 'Ada Lovelace', email: 'ada@codeforge.io', isActive: true, createdAt: 2024-03-15T09:00:00.000Z }
query: SELECT "user"."id", "user"."full_name", "user"."email", "user"."is_active", "user"."created_at" FROM "users" "user" WHERE "user"."is_active" = $1 ORDER BY "user"."created_at" DESC
Found 1 active user(s): [ { id: 1, fullName: 'Ada Lovelace', email: 'ada@codeforge.io', isActive: true, createdAt: 2024-03-15T09:00:00.000Z } ]
⚠️
Watch Out: reflect-metadata Must Be FirstImport 'reflect-metadata' at the very top of your application entry point — before any TypeORM import. If it's imported after, decorators silently lose their metadata and you'll get cryptic 'entity metadata not found' errors that are genuinely painful to debug.

Relationships: OneToMany and ManyToOne Done Right

Most real applications don't have isolated tables — they have relationships. A User has many Orders. An Order belongs to one User. Modelling these relationships in TypeORM is where things get both powerful and tricky.

TypeORM supports four relationship types: OneToOne, OneToMany, ManyToOne, and ManyToMany. The most common pairing you'll encounter in production is ManyToOne/OneToMany — they're always defined together, on both sides of the relationship.

The ManyToOne side (Order → User) holds the foreign key column in the database. The OneToMany side (User → Orders) is the inverse and exists only in TypeScript — it generates no extra column, it's purely a convenience for querying.

Critically, TypeORM uses lazy loading or eager loading to fetch related entities. The default is neither — TypeORM won't automatically load relations unless you explicitly ask for them via find options or the QueryBuilder. This is by design: it prevents accidental N+1 query disasters.

Understanding this opt-in loading model is the single most important thing to internalise about TypeORM relations. Every senior dev who's had to debug a production performance issue caused by an accidental 'eager: true' will nod vigorously at this.

OrderEntity.ts · TYPESCRIPT
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
import "reflect-metadata";
import {
  DataSource,
  Entity,
  PrimaryGeneratedColumn,
  Column,
  ManyToOne,
  OneToMany,
  JoinColumn,
  CreateDateColumn,
  Relation,
} from "typeorm";

// ─── User Entity (updated to include the OneToMany side) ──────────────────────
@Entity("users")
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  fullName: string;

  @Column({ unique: true })
  email: string;

  // OneToMany: one user can have many orders.
  // The string 'order.user' is the inverse side property name.
  // Relation<T[]> is preferred over T[] to avoid circular reference issues.
  @OneToMany(() => Order, (order) => order.user)
  orders: Relation<Order[]>; // no foreign key here — this is purely navigational
}

// ─── Order Entity ─────────────────────────────────────────────────────────────
@Entity("orders")
export class Order {
  @PrimaryGeneratedColumn()
  id: number;

  @Column("decimal", { precision: 10, scale: 2 })
  totalAmount: number; // e.g. 99.99 — use decimal, never float for money

  @Column({ length: 50, default: "pending" })
  status: string; // 'pending' | 'shipped' | 'delivered' | 'cancelled'

  @CreateDateColumn()
  placedAt: Date;

  // ManyToOne: many orders belong to one user.
  // This side owns the foreign key (user_id column will be created in 'orders' table).
  @ManyToOne(() => User, (user) => user.orders, {
    onDelete: "CASCADE", // if the user is deleted, their orders are deleted too
    nullable: false,     // every order MUST have a user
  })
  @JoinColumn({ name: "user_id" }) // explicit column name — don't let TypeORM guess
  user: Relation<User>;
}

// ─── DataSource with both entities ───────────────────────────────────────────
export const AppDataSource = new DataSource({
  type: "postgres",
  host: "localhost",
  port: 5432,
  username: "forge_user",
  password: "forge_pass",
  database: "forge_db",
  synchronize: true,
  logging: false, // turned off here for cleaner output — turn on when debugging
  entities: [User, Order], // both entities registered
});

async function demonstrateRelations() {
  await AppDataSource.initialize();

  const userRepo = AppDataSource.getRepository(User);
  const orderRepo = AppDataSource.getRepository(Order);

  // Step 1: Create a user
  const user = await userRepo.save(
    userRepo.create({ fullName: "Grace Hopper", email: "grace@codeforge.io" })
  );

  // Step 2: Create two orders belonging to that user
  // Pass the full user object — TypeORM extracts the foreign key automatically
  const order1 = await orderRepo.save(
    orderRepo.create({ totalAmount: 149.99, status: "shipped", user })
  );
  const order2 = await orderRepo.save(
    orderRepo.create({ totalAmount: 29.99, status: "pending", user })
  );

  console.log("Created orders:", order1.id, order2.id);

  // Step 3: Fetch the user WITH their orders — relations are opt-in!
  const userWithOrders = await userRepo.findOne({
    where: { id: user.id },
    relations: { orders: true }, // explicitly request the relation — no magic loading
  });

  console.log(
    `${userWithOrders?.fullName} has ${userWithOrders?.orders.length} orders:`
  );
  userWithOrders?.orders.forEach((order) => {
    console.log(`  Order #${order.id} — $${order.totalAmount} (${order.status})`);
  });

  await AppDataSource.destroy();
}

demonstateRelations().catch(console.error);
▶ Output
Created orders: 1 2
Grace Hopper has 2 orders:
Order #1 — $149.99 (shipped)
Order #2 — $29.99 (pending)
⚠️
Pro Tip: Use Relation Instead of T for Circular RefsWrapping relation types in Relation (imported from typeorm) prevents TypeScript circular reference issues when two entities reference each other. It's a small change that saves you from confusing 'Type alias circularly references itself' compiler errors as your entity graph grows.

QueryBuilder: Writing Complex Queries Without Losing Type Safety

The repository's find() method is great for straightforward fetches, but production applications routinely need multi-table joins, conditional filters, aggregations, and pagination. That's where TypeORM's QueryBuilder earns its place.

QueryBuilder lets you compose SQL programmatically using a fluent API — each method call adds a clause to the final query. Unlike raw SQL strings, QueryBuilder is aware of your entity structure, so mistyping a column name will often cause an error at build time rather than silently returning empty results.

The two most important patterns to understand are: using aliases consistently (the first argument to createQueryBuilder is the alias for the root entity), and using parameterised inputs with the :paramName syntax to prevent SQL injection. Never concatenate user input directly into a QueryBuilder expression — even if you're sure the value is safe today, future refactors won't be.

QueryBuilder also shines for pagination. Combining .skip() and .take() with .getManyAndCount() returns both the page of results and the total record count in a single round-trip — exactly what a REST API's paginated list endpoint needs.

Once you're comfortable with the basics, QueryBuilder also lets you drop into raw SQL for specific clauses via .andWhere(new Brackets(...)) or even .addSelect('raw SQL expression') when TypeORM's abstraction genuinely isn't expressive enough.

OrderQueries.ts · TYPESCRIPT
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
import { AppDataSource } from "./DataSource"; // your configured DataSource
import { Order } from "./OrderEntity";
import { User } from "./UserEntity";

async function runAdvancedQueries() {
  await AppDataSource.initialize();

  const orderRepo = AppDataSource.getRepository(Order);

  // ─── Example 1: Paginated list of shipped orders with user info ─────────────
  const pageNumber = 1;
  const pageSize = 10;

  const [shippedOrders, totalCount] = await orderRepo
    .createQueryBuilder("order") // 'order' is the alias for the Order entity
    .leftJoinAndSelect("order.user", "customer") // JOIN users table, alias it 'customer'
    .where("order.status = :status", { status: "shipped" }) // parameterised — safe
    .andWhere("customer.isActive = :isActive", { isActive: true })
    .orderBy("order.placedAt", "DESC")
    .skip((pageNumber - 1) * pageSize) // offset — skip records before this page
    .take(pageSize)                    // limit — return at most this many records
    .getManyAndCount();               // executes ONE query, returns [results, total]

  console.log(`Page ${pageNumber} of shipped orders (${totalCount} total):");
  shippedOrders.forEach((order) => {
    console.log(
      `  Order #${order.id} by ${order.user.fullName} — $${order.totalAmount}`
    );
  });

  // ─── Example 2: Aggregate — total revenue per user ─────────────────────────
  const revenueByUser = await AppDataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoin("user.orders", "order")           // JOIN without SELECT (no .AndSelect)
    .addSelect("SUM(order.totalAmount)", "totalRevenue") // raw aggregate
    .addSelect("COUNT(order.id)", "orderCount")
    .groupBy("user.id")
    .having("SUM(order.totalAmount) > :minRevenue", { minRevenue: 100 })
    .orderBy("totalRevenue", "DESC")
    .getRawMany(); // use getRawMany() for aggregates — getMany() won't include them

  console.log("\nTop customers by revenue:");
  revenueByUser.forEach((row) => {
    // getRawMany returns plain objects — column names are alias_column format
    console.log(
      `  ${row.user_fullName}: $${parseFloat(row.totalRevenue).toFixed(2)} across ${row.orderCount} orders`
    );
  });

  // ─── Example 3: Conditional query building (common in filter APIs) ──────────
  const statusFilter: string | null = "pending"; // imagine this comes from a request
  const userIdFilter: number | null = null;

  const dynamicQuery = orderRepo
    .createQueryBuilder("order")
    .leftJoinAndSelect("order.user", "customer");

  // Only add the WHERE clause if the filter is actually provided
  if (statusFilter) {
    dynamicQuery.andWhere("order.status = :status", { status: statusFilter });
  }
  if (userIdFilter) {
    dynamicQuery.andWhere("customer.id = :userId", { userId: userIdFilter });
  }

  const filteredOrders = await dynamicQuery.getMany();
  console.log(`\nDynamic filter returned ${filteredOrders.length} order(s)`);

  await AppDataSource.destroy();
}

runAdvancedQueries().catch(console.error);
▶ Output
Page 1 of shipped orders (3 total):
Order #7 by Grace Hopper — $149.99
Order #4 by Ada Lovelace — $89.50
Order #1 by Alan Turing — $55.00

Top customers by revenue:
Grace Hopper: $349.97 across 3 orders
Ada Lovelace: $179.49 across 2 orders

Dynamic filter returned 2 order(s)
🔥
Interview Gold: getMany() vs getRawMany()getMany() returns fully hydrated entity instances — TypeScript objects with all their class methods and decorator metadata. getRawMany() returns plain JavaScript objects with raw column aliases from the SQL. Use getRawMany() any time you use aggregate functions (SUM, COUNT, AVG) because getMany() simply can't map computed columns back onto entity shapes.

Migrations: The Right Way to Change Your Schema in Production

Synchronize: true is training wheels. It's genuinely useful when you're building quickly in development — TypeORM inspects your entities and auto-alters the schema to match. But in production, auto-sync is terrifying. A typo in a column name could drop and recreate that column, silently deleting data.

Migrations are the grown-up alternative. A migration is a timestamped TypeScript file with an up() method (apply the change) and a down() method (revert it). TypeORM tracks which migrations have run in a dedicated migrations table, so your schema history is version-controlled alongside your code — just like Git for your database.

The workflow is: change your entity, generate a migration with the CLI, review the generated SQL, commit the migration file, and let your deployment pipeline run it. Every environment gets the same schema changes in the same order.

This approach gives you rollback capability, a full audit trail, and the confidence to deploy schema changes without holding your breath. It's also what every serious engineering team uses — knowing migrations cold is a green flag in any technical interview.

MigrationWorkflow.ts · TYPESCRIPT
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// ─── Step 1: Configure DataSource for CLI use ─────────────────────────────────
// Create a dedicated file: src/data-source.ts
// The TypeORM CLI reads this file to know where your entities and migrations live.

import "reflect-metadata";
import { DataSource } from "typeorm";
import { User } from "./entities/UserEntity";
import { Order } from "./entities/OrderEntity";

export const AppDataSource = new DataSource({
  type: "postgres",
  host: process.env.DB_HOST ?? "localhost",
  port: parseInt(process.env.DB_PORT ?? "5432"),
  username: process.env.DB_USER ?? "forge_user",
  password: process.env.DB_PASS ?? "forge_pass",
  database: process.env.DB_NAME ?? "forge_db",

  synchronize: false,     // ALWAYS false once you're using migrations
  logging: ["migration"], // only log migration-related queries

  entities: [User, Order],

  migrations: ["src/migrations/*.ts"], // glob pattern — picks up all migration files
  migrationsTableName: "schema_migrations", // the table TypeORM uses to track runs
});

// ─── Step 2: Add CLI scripts to package.json ──────────────────────────────────
// "scripts": {
//   "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/migration -d src/data-source.ts",
//   "migration:run":      "typeorm-ts-node-commonjs migration:run -d src/data-source.ts",
//   "migration:revert":   "typeorm-ts-node-commonjs migration:revert -d src/data-source.ts"
// }

// ─── Step 3: Example of a generated migration file ───────────────────────────
// After running: npm run migration:generate AddPhoneToUsers
// TypeORM creates: src/migrations/1710500000000-AddPhoneToUsers.ts

import { MigrationInterface, QueryRunner } from "typeorm";

export class AddPhoneToUsers1710500000000 implements MigrationInterface {
  name = "AddPhoneToUsers1710500000000"; // timestamp in name ensures ordering

  // up(): what to do when this migration runs forward
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      // Nullable so existing rows aren't broken by the new column
      `ALTER TABLE "users" ADD "phone_number" character varying(20)`
    );
  }

  // down(): how to completely reverse the up() change — this is your rollback
  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `ALTER TABLE "users" DROP COLUMN "phone_number"`
    );
  }
}

// ─── Step 4: Run the migration ───────────────────────────────────────────────
// npm run migration:run
// TypeORM checks the schema_migrations table, sees this hasn't run, and executes up()

// ─── Step 5: Rollback if something went wrong ─────────────────────────────────
// npm run migration:revert
// TypeORM calls down() on the last applied migration — column is dropped safely
▶ Output
// npm run migration:run
query: SELECT * FROM "schema_migrations" WHERE "schema_migrations"."name" = 'AddPhoneToUsers1710500000000'
query: START TRANSACTION
query: ALTER TABLE "users" ADD "phone_number" character varying(20)
query: INSERT INTO "schema_migrations"("timestamp", "name") VALUES ($1, $2)
query: COMMIT
Migration AddPhoneToUsers1710500000000 has been executed successfully.

// npm run migration:revert
query: START TRANSACTION
query: ALTER TABLE "users" DROP COLUMN "phone_number"
query: DELETE FROM "schema_migrations" WHERE "name" = 'AddPhoneToUsers1710500000000'
query: COMMIT
Migration AddPhoneToUsers1710500000000 has been reverted successfully.
⚠️
Watch Out: Never Edit a Migration That's Already Run in ProductionOnce a migration has been committed and run in any shared environment, treat it as immutable. Editing it causes a hash mismatch — TypeORM may skip it or behave unpredictably. If you made a mistake, create a new migration to correct it. Your migration history is a permanent log of what happened, not a scratchpad.
AspectRepository (find/save)QueryBuilder
Best forSimple CRUD — findOne, find, save, deleteComplex joins, aggregates, dynamic filters
Type safetyFull — returns typed entity instancesPartial — getRawMany() returns plain objects
SQL controlLow — TypeORM decides the SQLHigh — you compose each clause
Learning curveGentle — intuitive APISteeper — requires SQL knowledge
PerformanceGood for most casesBetter for N-table joins and bulk ops
Paginationskip/take on find optionsskip/take with getManyAndCount()
Aggregates (SUM, COUNT)Not supportedFully supported via addSelect()
Conditional clausesLimited via where objectEasy — conditionally chain .andWhere()

🎯 Key Takeaways

  • Every Entity class is your schema documentation — decorators like @Column() and @ManyToOne() define both your TypeScript types and your database structure simultaneously, eliminating drift between code and schema.
  • Relations in TypeORM are never loaded automatically by default — you must opt in via 'relations' in find options or leftJoinAndSelect() in QueryBuilder. This protects you from accidental N+1 performance disasters.
  • Use the Repository API for CRUD and simple lookups; reach for QueryBuilder when you need joins across multiple tables, aggregate functions (SUM, COUNT), or dynamically composed WHERE clauses.
  • Migrations are non-negotiable in production — synchronize: true is a development convenience that can silently destroy data in production. Version-control your migrations alongside your code so every environment gets the same schema history.

⚠ Common Mistakes to Avoid

  • Mistake 1: Leaving synchronize: true in production config — Symptom: A renamed entity property causes TypeORM to DROP the old column and CREATE a new one, silently destroying data on the next deploy — Fix: Set synchronize: false immediately and switch to migrations. Use environment variables so dev can still use sync but prod never does: synchronize: process.env.NODE_ENV !== 'production'
  • Mistake 2: Forgetting to register new entities in the DataSource — Symptom: TypeORM throws 'No metadata for Entity was found' at runtime, even though your class is decorated correctly — Fix: Every entity class must appear in the entities array of your DataSource config. A glob pattern like entities: ['src/entities/*.ts'] is safer than manually listing them — it automatically picks up new files.
  • Mistake 3: Using getMany() instead of getRawMany() for aggregate queries — Symptom: Your SUM or COUNT columns come back as undefined even though the SQL is correct — Fix: Any query using aggregate functions (SUM, AVG, COUNT) or .addSelect() with computed expressions must use getRawMany(). Only use getMany() when you're fetching actual entity rows that map directly to your class properties.

Interview Questions on This Topic

  • QWhat's the difference between .save() and .insert() in TypeORM, and when would you choose one over the other?
  • QHow does TypeORM handle the N+1 query problem, and what patterns do you use to prevent it in production?
  • QIf you have synchronize: true in dev and migrations in prod, how do you keep the two in sync — and what can go wrong if you don't?

Frequently Asked Questions

Does TypeORM work with both TypeScript and JavaScript?

Yes — TypeORM supports both, but the TypeScript experience is dramatically better. Decorators like @Entity() and @Column() rely on TypeScript's emitDecoratorMetadata to attach type information at runtime. In plain JavaScript you'd need to use the EntitySchema alternative, which is more verbose and loses the 'class as documentation' benefit that makes TypeORM compelling.

What's the difference between TypeORM's ActiveRecord and DataMapper patterns?

ActiveRecord puts database logic directly on the entity class — your User entity would have a User.find() static method. DataMapper separates concerns — your User class is a plain data object, and a separate Repository handles all database operations. TypeORM supports both, but DataMapper (via repositories and DataSource) is recommended for larger applications because it's easier to test and keeps your domain models clean.

Why do TypeORM relations come back as undefined even when I've set up the decorators correctly?

Because TypeORM doesn't load relations automatically — this is intentional, not a bug. You must explicitly request them either via the 'relations' option in findOne/find (e.g., relations: { orders: true }), or via leftJoinAndSelect() in QueryBuilder. Without one of those, the relation property will be undefined even though the foreign key exists in the database.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousMySQL Replication SetupNext →SQL Date and Time Functions
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged