codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›backend

TypeORM and read-only entities

Published 2026-04-28· Updated 2026-05-18·0 views

TypeORM and read-only entities

TypeORM was once a representative ORM in the Node camp and was widely used alongside NestJS. Its position has shifted somewhat as new ORMs appeared, but it still runs in many codebases.

1. About TypeORM

TypeORM is reportedly first released around 2016 as a TypeScript/JavaScript ORM. It supports a wide range of RDBs — PostgreSQL, MySQL, MariaDB, SQLite, MS SQL Server, Oracle — and some NoSQL (MongoDB).

Library First appeared Character
TypeORM 2016 Decorator based. Supports both AR and DM.
Sequelize 2010 The longest-running Node ORM. JS first.
MikroORM 2018 TS first. Identity Map · Unit of Work.
Prisma 2019 (Prisma 1) → 2020 (current Prisma) Schema file + generated client.
Drizzle ORM 2022 TS schema code. SQL-like builder.
Kysely 2022 Type-safe query builder (strictly not an ORM).

2. Active Record vs Data Mapper

Two patterns organized in Martin Fowler's Patterns of Enterprise Application Architecture (2002).

  • Active Record — the entity object saves itself. user.save().
  • Data Mapper — a separate mapper or repository persists the entity. repo.save(user).

TypeORM provides both.

// Active Record
@Entity()
class User extends BaseEntity {
  @PrimaryGeneratedColumn() id!: number
  @Column() email!: string
}
const u = User.create({ email: 'a@b' })
await u.save()

// Data Mapper
const repo = dataSource.getRepository(User)
await repo.save(repo.create({ email: 'a@b' }))

In environments that emphasize service-layer separation and testability, Data Mapper is often the choice.

3. Entity → read-only SSOT pattern

TypeORM can synchronize the schema from entity metadata (synchronize: true) or generate migrations (migration:generate). In production, two directions commonly emerge.

① TypeORM is the truth of the schema — entities are SSOT, migrations auto-generated
② SQL is the truth of the schema — entities are mappings only, schema changes happen in SQL files

In case (2), entity decorators express only column names, types, and relations, and synchronize: true is never enabled. Production DB changes are made only via separate SQL or a migration tool. Strengths:

  • DBAs and data teams can work directly in SQL.
  • It blocks dangerous auto-generated migration changes (column drops, type changes).
  • When the same DB is shared across languages or services, the source of truth lives in one place.

The downside is that entities can drift from the actual DB. A procedure of integration tests verifying both sides match is needed.

4. Relations and N+1

@Entity()
class Order {
  @ManyToOne(() => User) user!: User
}

const orders = await repo.find()
for (const o of orders) {
  console.log(o.user.email)  // lazy load: SELECT each time
}

This pattern is the classic N+1 problem. One main query + N auxiliary queries. The fix is explicit join.

const orders = await repo.find({ relations: { user: true } })
// or query builder
const orders = await repo.createQueryBuilder('o')
  .leftJoinAndSelect('o.user', 'u')
  .getMany()

Or introduce a batch loader pattern like DataLoader (common in GraphQL environments).

5. Comparison with Prisma · Drizzle · MikroORM

Prisma — schema.prisma is the SSOT, with a generated client. Type inference is strong and migrations are well organized. The runtime used to go through a query engine (a Rust binary), and recent direction has been to slim the engine.

Drizzle — an SQL-like builder. Schema lives in TS code. Runtime overhead is small and type inference is strong.

MikroORM — implements enterprise patterns like Identity Map and Unit of Work faithfully. Tracks entity changes and flushes at once.

Sequelize — stable due to its age, but type inference is reportedly weaker than the three above.

The choice depends on the team's SQL familiarity, migration approach, and required level of type safety.

6. DataSource and Repository

DataSource is an object holding the connection pool and metadata. It is initialized once at app start and referenced globally.

export const ds = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [User, Order],
  migrations: ['migrations/*.ts'],
  synchronize: false,   // false in production
})
await ds.initialize()
const userRepo = ds.getRepository(User)
class UserService {
  constructor(private repo = userRepo) {}
  byEmail(email: string) { return this.repo.findOne({ where: { email } }) }
}

Both keeping repository methods inside services and extracting them to separate classes are valid.

7. Common pitfalls

Applying synchronize: true to production — columns can vanish. Always off in production.

Missing @JoinColumn — in ManyToOne relations, the foreign key column name can drift from intent. Specifying it is recommended.

Circular imports — bidirectional relations between entities can be made lazy with a function reference (() => Other) to avoid cycles.

Timezone columns — accurately reflect the difference between PostgreSQL's timestamptz and timestamp in the entity decorators. Misalignment causes time drift.

Transaction boundaries — to wrap several repositories in a single transaction, use a manager-based repository inside dataSource.transaction(async (manager) => ...).

Closing thoughts

ORMs are a tradeoff between the value of abstraction and the clarity of raw SQL. A small team typically lowers operational burden by starting with raw SQL, while bigger teams or richer domains start to see the abstraction gain. The "entities map only, schema in SQL" pattern sits close to the middle ground.

Next

  • crawler-ethics
  • openapi-spec

See TypeORM · TypeORM GitHub · Patterns of Enterprise Application Architecture · Prisma · Drizzle ORM · MikroORM · DataLoader.

More in backend

All in this category →
  • Wrap public OpenAPIs with your own BFF
  • Email Delivery and OTP — SMTP
  • Audit Log — logAdminAction pattern
  • WebSocket and SSE — real-time communication
  • REST API introduction
  • OpenAPI Specification