database/typeorm-starter/SKILL.md
Scaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.
npx skillsauth add achreftlili/deep-dev-skills typeorm-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold a TypeORM 0.3+ project with TypeScript, entities with decorators, CLI migrations, repositories, DataSource configuration, relations, query builder, and subscribers.
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)
# 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
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
DataSource config file that is used by both the application and the CLI.@Entity, @Column, @PrimaryGeneratedColumn, etc.).dataSource.getRepository(Entity) — the old getConnection() API is removed in 0.3+.reflect-metadata once at the application entry point before any TypeORM usage.@CreateDateColumn and @UpdateDateColumn for automatic timestamps.QueryBuilder for complex queries; use repository methods for simple CRUD.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"],
});
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 };
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;
}
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;
}
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[];
}
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) };
}
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();
}
}
}
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 },
});
});
}
# 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
@nestjs/typeorm with TypeOrmModule.forRootAsync() and TypeOrmModule.forFeature([Entity]). Pair with nestjs-project-starter skill.AppDataSource.initialize() in the server bootstrap before handling requests.AppDataSource.query('TRUNCATE ... CASCADE').docker-compose-generator skill for the database service. Run migrations in a startup script or init container.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.