CtrlK
BlogDocsLog inGet started
Tessl Logo

typeorm-starter

Scaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.

79

Quality

73%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./database/typeorm-starter/SKILL.md
SKILL.md
Quality
Evals
Security

TypeORM Starter

Scaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.

Prerequisites

  • Node.js >= 20.x
  • TypeScript >= 5.3
  • PostgreSQL, MySQL, or SQLite database
  • npm or pnpm

Scaffold Command

npm install typeorm reflect-metadata pg
npm install -D typescript @types/node tsx

# Ensure tsconfig.json has these compiler options:
# "emitDecoratorMetadata": true
# "experimentalDecorators": true
# "strictPropertyInitialization": false (for entity columns)

Database Initialization

# Synchronize schema (development only — never use in production)
npx typeorm schema:sync

# Generate migration from entity changes
npx typeorm migration:generate -d src/data-source.ts src/migrations/Init

# Run pending migrations
npx typeorm migration:run -d src/data-source.ts

Project Structure

src/
  data-source.ts                  # DataSource configuration (single source of truth)
  entities/
    user.entity.ts
    post.entity.ts
    tag.entity.ts
  migrations/
    1700000000000-InitialSchema.ts
  subscribers/
    user.subscriber.ts
  repositories/
    user.repository.ts
  lib/
    database.ts                   # Initialize and export DataSource

Key Conventions

  • One DataSource config file that is used by both the application and the CLI.
  • Entities use TypeORM decorators (@Entity, @Column, @PrimaryGeneratedColumn, etc.).
  • Use the repository pattern via dataSource.getRepository(Entity) — the old getConnection() API is removed in 0.3+.
  • Generate migrations with the CLI, never write them by hand unless necessary.
  • Import reflect-metadata once at the application entry point before any TypeORM usage.
  • Use @CreateDateColumn and @UpdateDateColumn for automatic timestamps.
  • Prefer QueryBuilder for complex queries; use repository methods for simple CRUD.

Essential Patterns

DataSource Configuration (src/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", 10),
  username: process.env.DB_USERNAME ?? "app",
  password: process.env.DB_PASSWORD ?? "secret",
  database: process.env.DB_DATABASE ?? "myapp",
  synchronize: false, // Never true in production
  logging: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"],
  entities: ["src/entities/**/*.entity.ts"],
  migrations: ["src/migrations/**/*.ts"],
  subscribers: ["src/subscribers/**/*.ts"],
});

Database Initialization (src/lib/database.ts)

import { AppDataSource } from "../data-source";

export async function initializeDatabase(): Promise<void> {
  if (!AppDataSource.isInitialized) {
    await AppDataSource.initialize();
    console.log("Database connection established");
  }
}

export { AppDataSource };

User Entity (src/entities/user.entity.ts)

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
  OneToOne,
  Index,
} from "typeorm";
import { Post } from "./post.entity";
import { Profile } from "./profile.entity";

export enum UserRole {
  USER = "user",
  ADMIN = "admin",
}

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

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

  @Column()
  name: string;

  @Column({ select: false })
  password: string;

  @Column({ type: "enum", enum: UserRole, default: UserRole.USER })
  role: UserRole;

  @OneToMany(() => Post, (post) => post.author)
  posts: Post[];

  @OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
  profile: Profile;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Post Entity with Relations (src/entities/post.entity.ts)

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  ManyToMany,
  JoinTable,
  Index,
} from "typeorm";
import { User } from "./user.entity";
import { Tag } from "./tag.entity";

export enum PostStatus {
  DRAFT = "draft",
  PUBLISHED = "published",
  ARCHIVED = "archived",
}

@Entity("posts")
export class Post {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  title: string;

  @Column({ type: "text", nullable: true })
  content: string | null;

  @Column({ type: "enum", enum: PostStatus, default: PostStatus.DRAFT })
  @Index()
  status: PostStatus;

  @ManyToOne(() => User, (user) => user.posts, { onDelete: "CASCADE" })
  @Index()
  author: User;

  @Column()
  authorId: string;

  @ManyToMany(() => Tag, (tag) => tag.posts)
  @JoinTable({ name: "posts_tags" })
  tags: Tag[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

Tag Entity (src/entities/tag.entity.ts)

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Post } from "./post.entity";

@Entity("tags")
export class Tag {
  @PrimaryGeneratedColumn("uuid")
  id: string;

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

  @ManyToMany(() => Post, (post) => post.tags)
  posts: Post[];
}

Repository Pattern (src/repositories/user.repository.ts)

import { AppDataSource } from "../data-source";
import { User, UserRole } from "../entities/user.entity";

const userRepo = AppDataSource.getRepository(User);

export async function createUser(data: {
  email: string;
  name: string;
  password: string;
}): Promise<User> {
  const user = userRepo.create(data);
  return userRepo.save(user);
}

export async function findUserByEmail(email: string): Promise<User | null> {
  return userRepo.findOne({
    where: { email },
    relations: { profile: true },
  });
}

export async function findUserWithPosts(userId: string): Promise<User | null> {
  return userRepo.findOne({
    where: { id: userId },
    relations: { posts: true },
  });
}

// Query Builder for complex queries
export async function searchUsers(params: {
  query?: string;
  role?: UserRole;
  page: number;
  limit: number;
}) {
  const qb = userRepo
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.profile", "profile")
    .loadRelationCountAndMap("user.postCount", "user.posts");

  if (params.query) {
    qb.andWhere("(user.name ILIKE :q OR user.email ILIKE :q)", {
      q: `%${params.query}%`,
    });
  }

  if (params.role) {
    qb.andWhere("user.role = :role", { role: params.role });
  }

  const [users, total] = await qb
    .orderBy("user.createdAt", "DESC")
    .skip((params.page - 1) * params.limit)
    .take(params.limit)
    .getManyAndCount();

  return { users, total, pages: Math.ceil(total / params.limit) };
}

Subscriber (src/subscribers/user.subscriber.ts)

import {
  EventSubscriber,
  EntitySubscriberInterface,
  InsertEvent,
  UpdateEvent,
} from "typeorm";
import { User } from "../entities/user.entity";

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  listenTo() {
    return User;
  }

  beforeInsert(event: InsertEvent<User>): void {
    if (event.entity.email) {
      event.entity.email = event.entity.email.toLowerCase().trim();
    }
  }

  beforeUpdate(event: UpdateEvent<User>): void {
    if (event.entity?.email) {
      event.entity.email = event.entity.email.toLowerCase().trim();
    }
  }
}

Transaction Example

import { AppDataSource } from "../data-source";
import { User } from "../entities/user.entity";
import { Profile } from "../entities/profile.entity";

export async function createUserWithProfile(data: {
  email: string;
  name: string;
  password: string;
  bio: string;
}): Promise<User> {
  return AppDataSource.transaction(async (manager) => {
    const user = manager.create(User, {
      email: data.email,
      name: data.name,
      password: data.password,
    });
    await manager.save(user);

    const profile = manager.create(Profile, {
      bio: data.bio,
      userId: user.id,
    });
    await manager.save(profile);

    return manager.findOneOrFail(User, {
      where: { id: user.id },
      relations: { profile: true },
    });
  });
}

Common Commands

# Generate a migration from entity changes
npx typeorm migration:generate src/migrations/MigrationName -d src/data-source.ts

# Create an empty migration (for manual SQL)
npx typeorm migration:create src/migrations/MigrationName

# Run pending migrations
npx typeorm migration:run -d src/data-source.ts

# Revert the last migration
npx typeorm migration:revert -d src/data-source.ts

# Show migration status
npx typeorm migration:show -d src/data-source.ts

# Sync schema (dev only — never in production)
npx typeorm schema:sync -d src/data-source.ts

# Drop all tables
npx typeorm schema:drop -d src/data-source.ts

Integration Notes

  • NestJS: Use @nestjs/typeorm with TypeOrmModule.forRootAsync() and TypeOrmModule.forFeature([Entity]). Pair with nestjs-project-starter skill.
  • Express/Fastify: Call AppDataSource.initialize() in the server bootstrap before handling requests.
  • Testing: Use a separate test database. Run migrations before tests, truncate tables between test suites using AppDataSource.query('TRUNCATE ... CASCADE').
  • Prisma migration: TypeORM and Prisma should not coexist. Choose one ORM per project.
  • Docker: Pair with docker-compose-generator skill for the database service. Run migrations in a startup script or init container.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.