Настройка ORM TypeORM для веб-приложения

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Настройка ORM TypeORM для веб-приложения
Средняя
~1 рабочий день
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Настройка ORM TypeORM для веб-приложения

TypeORM — один из старейших ORM для TypeScript/Node.js. Поддерживает Active Record и Data Mapper паттерны, декораторы для определения сущностей и богатый набор функций: миграции, subscribers, relations, query builder. Широко используется в NestJS-проектах — там это де-факто стандарт.

Установка

npm install typeorm reflect-metadata
npm install pg  # PostgreSQL
# или mysql2, better-sqlite3, mongodb

# В tsconfig.json
{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

Подключение к базе

// db/data-source.ts
import 'reflect-metadata'
import { DataSource } from 'typeorm'
import { User } from './entities/User'
import { Post } from './entities/Post'

export const AppDataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [User, Post],
  migrations: ['dist/db/migrations/*.js'],
  migrationsTableName: 'migrations',
  synchronize: false,  // НИКОГДА true в production
  logging: process.env.NODE_ENV === 'development' ? ['query', 'error'] : ['error'],
  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
  extra: {
    max: 20,
    idleTimeoutMillis: 30000,
  }
})

// Инициализация
await AppDataSource.initialize()

Сущности

// db/entities/User.ts
import {
  Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
  UpdateDateColumn, OneToMany, Index, BeforeInsert, BeforeUpdate
} from 'typeorm'
import bcrypt from 'bcrypt'
import { Post } from './Post'

export enum UserRole {
  USER = 'user',
  MODERATOR = 'moderator',
  ADMIN = 'admin',
}

@Entity('users')
@Index(['email'])
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ length: 255, unique: true })
  email: string

  @Column({ length: 255 })
  name: string

  @Column({ select: false })  // не включать в SELECT по умолчанию
  passwordHash: string

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

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

  @CreateDateColumn({ type: 'timestamptz' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamptz' })
  updatedAt: Date

  @BeforeInsert()
  @BeforeUpdate()
  async hashPassword() {
    if (this.passwordHash && !this.passwordHash.startsWith('$2b$')) {
      this.passwordHash = await bcrypt.hash(this.passwordHash, 12)
    }
  }
}
// db/entities/Post.ts
import {
  Entity, PrimaryGeneratedColumn, Column, CreateDateColumn,
  UpdateDateColumn, ManyToOne, ManyToMany, JoinTable, JoinColumn, Index
} from 'typeorm'
import { User } from './User'
import { Tag } from './Tag'

@Entity('posts')
@Index(['authorId', 'createdAt'])
@Index(['published', 'createdAt'])
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Column({ type: 'text' })
  title: string

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

  @Column({ default: false })
  published: boolean

  @Column({ name: 'author_id' })
  authorId: string

  @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'author_id' })
  author: User

  @ManyToMany(() => Tag, (tag) => tag.posts, { cascade: true })
  @JoinTable({
    name: 'posts_to_tags',
    joinColumn: { name: 'post_id' },
    inverseJoinColumn: { name: 'tag_id' }
  })
  tags: Tag[]

  @Column({ default: 0 })
  viewCount: number

  @Column({ type: 'timestamptz', nullable: true })
  publishedAt: Date | null

  @CreateDateColumn({ type: 'timestamptz' })
  createdAt: Date

  @UpdateDateColumn({ type: 'timestamptz' })
  updatedAt: Date
}

Миграции

# Генерация миграции из diff схемы
npx typeorm migration:generate -n AddUserProfile -d dist/db/data-source.js

# Создать пустую миграцию вручную
npx typeorm migration:create -n AddIndexes

# Применить
npx typeorm migration:run -d dist/db/data-source.js

# Откатить последнюю
npx typeorm migration:revert -d dist/db/data-source.js
// db/migrations/1234567890-AddUserProfile.ts
import { MigrationInterface, QueryRunner } from 'typeorm'

export class AddUserProfile1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE profiles (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        user_id UUID NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
        bio TEXT,
        avatar_url VARCHAR(500),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
      )
    `)
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE profiles`)
  }
}

Query Builder

const postRepository = AppDataSource.getRepository(Post)

// Поиск с пагинацией
async function findPosts(opts: { page: number; limit: number; search?: string }) {
  const { page, limit, search } = opts
  const qb = postRepository.createQueryBuilder('post')
    .leftJoinAndSelect('post.author', 'author')
    .leftJoinAndSelect('post.tags', 'tag')
    .where('post.published = :published', { published: true })
    .orderBy('post.createdAt', 'DESC')
    .skip((page - 1) * limit)
    .take(limit)

  if (search) {
    qb.andWhere(
      'post.title ILIKE :search OR post.content ILIKE :search',
      { search: `%${search}%` }
    )
  }

  const [items, total] = await qb.getManyAndCount()
  return { items, total, pages: Math.ceil(total / limit) }
}

// Сложные агрегаты
const stats = await AppDataSource.query(`
  SELECT
    date_trunc('week', created_at) AS week,
    count(*) AS posts,
    count(*) FILTER (WHERE published = true) AS published
  FROM posts
  WHERE created_at >= now() - interval '90 days'
  GROUP BY 1
  ORDER BY 1
`)

Subscribers (аналог хуков)

import { EntitySubscriberInterface, EventSubscriber, InsertEvent, UpdateEvent } from 'typeorm'

@EventSubscriber()
export class PostSubscriber implements EntitySubscriberInterface<Post> {
  listenTo() { return Post }

  async afterInsert(event: InsertEvent<Post>) {
    await SearchIndexer.index('posts', event.entity)
  }

  async afterUpdate(event: UpdateEvent<Post>) {
    if (event.updatedColumns.some(c => c.propertyName === 'published')) {
      await NotificationService.notifyFollowers(event.entity)
    }
  }
}

NestJS интеграция

// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        url: config.get('DATABASE_URL'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        migrations: [__dirname + '/db/migrations/*{.ts,.js}'],
        migrationsRun: true,
        synchronize: false,
      })
    }),
    TypeOrmModule.forFeature([User, Post])
  ]
})
export class AppModule {}

Сроки

Базовая настройка TypeORM с сущностями, миграциями и репозиториями: 1–2 дня. Интеграция в NestJS-проект с модулями и тестами: 2–3 дня. Перевод существующего проекта с другого ORM на TypeORM: 3–5 дней.