Naalya Handbook

Entities, Tables & Types

How one TypeScript interface ties a database table, its entity, and its response DTO together so they can never drift.

Each resource exists in three places: a PostgreSQL table, a TypeORM entity that maps it, and a response DTO that goes over the wire. A single plain-TypeScript interface owns the shape; the entity and the DTO both implements it, so neither can drift from the contract without a build break.

Modeling a resource is mechanical: name the interface first, then the two classes follow.

PieceFileRole
Interfacedto/<module>.types.tsThe contract — pure TS, no decorators
Entityentities/<resource>.entity.tsimplements it + TypeORM column decorators
Response DTOdto/<resource>.dto.tsimplements it + validator/Swagger decorators

The contract

Lives in <module>/dto/<module>.types.ts. Pure TypeScript — no TypeORM, Swagger, or DB/HTTP imports. extends DatabaseEntity folds in the UUID id and timestamp types, exported as a type.

apps/server/src/app/campus/dto/campus.types.ts
interface Campus extends DatabaseEntity {
  name: string;
  description?: string;
  address: Address;
  slug: string;
  admissionFee?: number;
  // ...
}

Add, rename, or make a field optional in the interface — never edit the entity and DTO directly. The compiler then walks you through both.

Entity maps the table

implements the interface (shape is checked against the contract) and carries decorators that map each property to a column.

apps/server/src/app/campus/entities/campus.entity.ts
@Entity('campus')
@TenantScoped()
@WithTimestamps()
export class CampusEntity extends DatabaseEntity implements Campus {
  @Column()
  name: string;

  @Column({ unique: true })
  slug: string;
  // ...
}
  • @Entity('campus') names the table; each @Column() becomes a column.
  • extends DatabaseEntity supplies the UUID id.
  • @WithTimestamps() registers created_at / updated_at / deleted_at — applied last so they land at the table's end.
  • @TenantScoped() is the multi-tenancy hook — see The Database.
  • Year-owned entities (enrollment, grade, exams…) also declare a non-null campusAcademicYearId column — a convention, not a decorator; see Scoped Entities.
  • Drop a contract field and the build fails: implements blocks divergence.

Columns and types

Columns are snake_case in PostgreSQL, camelCase in TS. The global SnakeNamingStrategy maps the two automatically. Set name: only to override it.

@Column() options reached for constantly: nullable, unique, default, length, type. An enum discriminator tags which kind of row a record is.

apps/server/src/app/user/entities/user.entity.ts
@Column({ unique: true })
email: string;

@Column({ type: 'enum', enum: UserType })
type: UserType;

@Column({ name: 'is_active', default: true })
isActive: boolean;

admissionFee in the class becomes admission_fee in the table automatically. Set name: only to override the strategy, never to restate it.

The shared base types

Two base types live side by side in one file:

  • DatabaseEntity — entity-side base. Owns the UUID id and declares the timestamp types (@WithTimestamps() registers the real columns).
  • DatabaseEntityDto — response-side twin. implements DatabaseEntity, decorating each field for validation and Swagger.
libs/shared/src/database/database.entity.ts
abstract class DatabaseEntityDto implements DatabaseEntity {
  @IsUUID()
  @ApiProperty({ description: 'ID' })
  id: string;

  @IsDate()
  @ApiProperty({ description: 'Date of creation' })
  createdAt: Date;
  // updatedAt, deletedAt ...
}

A DTO extends DatabaseEntityDto to inherit a documented, validated id and timestamps. The interface extends DatabaseEntity, so those fields are part of every contract — defined once, ride along everywhere.

DTO maps the wire

implements the interface like the entity, but carries class-validator + Swagger decorators describing the JSON shape. Same field list, different decorators. extends DatabaseEntityDto brings in id and timestamps.

apps/server/src/app/campus/dto/campus.dto.ts
export class CampusDto extends DatabaseEntityDto implements Campus {
  @IsString()
  @IsNotEmpty()
  @ApiProperty()
  name: string;

  @IsNumber()
  @IsOptional()
  @ApiPropertyOptional({ required: false })
  admissionFee?: number;
  // ...
}

Validator and @ApiProperty rules in depth → DTOs & Validation.

The entity and the DTO never reference each other — only the interface. So they can't agree with each other while disagreeing with the truth.

Relations in brief

Links are entity decorators: @ManyToOne, @OneToOne, @OneToMany, paired with @JoinColumn to name the foreign-key column.

apps/server/src/app/campus/entities/campus.entity.ts
@OneToOne(() => AddressEntity, (address) => address.id, {
  eager: true,
  onDelete: 'SET NULL',
  nullable: true,
})
@JoinColumn({ name: 'address_id' })
address: AddressEntity;
  • Lazy relations are typed Promise<T> and must be awaited before reading (e.g. UserEntity profile links: { lazy: true }, typed Promise<StaffProfileEntity>).
  • Many-to-many is never implicit: an explicit <a>-x-<b> join entity holds two foreign keys with a @Unique on the pair.

The discriminator-plus-join-table machinery (how UserType decides which profile to load) → Module Anatomy.

Where to go next

On this page