Skip to content
NestJS ns database 5 min read

TypeORM Entities

In TypeORM an entity is a plain TypeScript class decorated so the ORM can map it to a database table. Each instance is a row, each decorated property is a column, and the decorators carry enough metadata that TypeORM can generate schema, build queries, and hydrate results into typed objects. Modeling tables well up front pays off everywhere downstream: your repositories, query builders, and migrations all derive their shape from these classes, so getting columns, keys, constraints, and indexes right keeps the rest of your data layer honest.

Declaring an entity

Mark a class with @Entity() to register it as a mapped table. By default the table name is derived from the class name (lower-cased), but you can override it explicitly — recommended for stable schemas that you don’t want renamed by an accidental class rename.

// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ length: 120 })
  name: string;

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

  @Column({ default: true })
  isActive: boolean;
}

For TypeORM to read these decorators you need experimentalDecorators and emitDecoratorMetadata enabled in tsconfig.json — the Nest CLI sets both by default.

Primary keys and generated columns

Every entity needs a primary key. @PrimaryGeneratedColumn produces an auto-incrementing or generated key; pass a strategy to control how the value is created. For distributed systems prefer 'uuid' so IDs don’t collide across nodes and aren’t guessable in URLs.

DecoratorStrategyResulting column
@PrimaryGeneratedColumn()increment (default)Auto-increment integer
@PrimaryGeneratedColumn('uuid')uuidUUID string
@PrimaryGeneratedColumn('rowid')rowidCockroachDB row id
@PrimaryColumn()noneManually assigned primary key

You can also compose a key from several columns by applying @PrimaryColumn() to more than one property — useful for join tables and natural composite keys.

@Entity('enrollments')
export class Enrollment {
  @PrimaryColumn('uuid')
  studentId: string;

  @PrimaryColumn('uuid')
  courseId: string;
}

Column types and options

@Column() maps a property to a column. With reflection TypeORM infers a sensible type from the TypeScript type, but always set an explicit type for anything non-trivial (decimals, JSON, enums) so the generated schema matches across databases. The options object covers nullability, length, precision, defaults, and database-specific names.

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

export enum Role {
  Admin = 'admin',
  Member = 'member',
}

@Entity('accounts')
export class Account {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 200, nullable: false })
  name: string;

  @Column({ type: 'enum', enum: Role, default: Role.Member })
  role: Role;

  @Column({ type: 'decimal', precision: 12, scale: 2, default: 0 })
  balance: string;

  @Column({ type: 'jsonb', nullable: true })
  metadata: Record<string, unknown> | null;

  @Column({ name: 'created_at', type: 'timestamptz', default: () => 'now()' })
  createdAt: Date;
}
OptionPurpose
typeExplicit database column type (varchar, int, jsonb, enum, …)
namePhysical column name when it differs from the property
nullableAllows NULL; defaults to false
defaultDefault value or a function for SQL expressions
uniqueAdds a single-column unique constraint
lengthMax length for string types
precision / scaleDigits and decimal places for numeric types

Map SQL decimal/numeric columns to a string property, not number. JavaScript’s floating point cannot represent monetary values exactly, and TypeORM returns these columns as strings to preserve precision.

Unique constraints and indexes

Single-column uniqueness is fastest to express with unique: true on the column. For multi-column uniqueness or named constraints, use the class-level @Unique() decorator. Indexes — which speed up reads on frequently filtered columns — are declared with @Index, either on a property or on the class for composite indexes.

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  Index,
  Unique,
} from 'typeorm';

@Entity('memberships')
@Unique('uq_membership_user_org', ['userId', 'orgId'])
@Index('idx_membership_org_active', ['orgId', 'isActive'])
export class Membership {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('uuid')
  userId: string;

  @Column('uuid')
  orgId: string;

  @Index()
  @Column({ unique: true })
  inviteCode: string;

  @Column({ default: true })
  isActive: boolean;
}

The composite @Index(['orgId', 'isActive']) accelerates a query that filters memberships by organization and active flag together, while @Unique(['userId', 'orgId']) guarantees a user joins an organization only once. Name your constraints and indexes explicitly — auto-generated names are unstable and make migration diffs noisy.

Registering entities with Nest

Once defined, entities are wired into the connection so repositories can be injected. Register feature entities with TypeOrmModule.forFeature() in their owning module; with autoLoadEntities: true in the root config you don’t also have to list them in forRoot.

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { UsersService } from './users.service';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
})
export class UsersModule {}

In local development you can let TypeORM print the schema it derives from your entities:

Output:

query: CREATE TABLE "users" (
  "id" uuid NOT NULL DEFAULT uuid_generate_v4(),
  "name" character varying(120) NOT NULL,
  "email" character varying NOT NULL,
  "isActive" boolean NOT NULL DEFAULT true,
  CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email"),
  CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id")
)

Best practices

  • Pass an explicit table name to @Entity('table_name') so a class rename never silently renames your table.
  • Set an explicit type for decimals, enums, JSON, and dates instead of relying on inference — it keeps schema consistent across databases.
  • Use @PrimaryGeneratedColumn('uuid') for public-facing IDs to avoid sequential, guessable keys.
  • Store money and other exact decimals as string properties to preserve precision.
  • Name unique constraints and indexes (@Unique('uq_...'), @Index('idx_...')) so migrations produce clean, stable diffs.
  • Add composite indexes for the column combinations your hot queries actually filter on, and avoid over-indexing columns you never query.
  • Keep schema changes under migrations rather than synchronize: true once you move past local development.
Last updated June 14, 2026
Was this helpful?