Skip to content

feat(nestjs): integrate TypeORM for database persistence #12

Description

@dev-queiroz

Summary

Replace NestJS generator's in-memory array storage with real database persistence using TypeORM. This is critical for production readiness and enables data integrity, transactions, and multi-instance deployments.

Motivation

Current NestJS generation produces a demo-quality project with hardcoded in-memory storage:

// Current - loses data on restart
private readonly items: Produto[] = [];

This severely limits usefulness:

  • No persistence between restarts
  • Single-process only (no scaling)
  • No transactions or ACID guarantees
  • No real database schema
  • Cannot demonstrate production patterns

With TypeORM integration, generated projects become production-ready:

  • SQL database backend (PostgreSQL, MySQL, SQLite)
  • Entity relationship mapping
  • Migrations and schema management
  • Transaction support
  • Query optimization

Scope

In Scope

  • Generate TypeORM entity classes from Forge contracts
  • Entity decorators: @entity, @column, @PrimaryGeneratedColumn
  • Proper column types matching Forge types
  • Repository pattern implementation
  • Database connection configuration
  • SQLite as default for local development
  • PostgreSQL configuration for production
  • Generate migration setup instructions
  • TypeORM installation and configuration

Out of Scope

  • Relationships/foreign keys (v0.3 Feature 2.2)
  • Cascade operations
  • Query optimization/indexes
  • Database connection pooling
  • Migrations generation from schema changes

Technical Design

Generated Entity Example

Input:

namespace financeiro

contract Produto {
  id: uuid
  nome: string
  descricao?: string
  valor: decimal
  ativo: boolean
  criadoEm: datetime
}

Output (src/produto/entities/produto.entity.ts):

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

@Entity('financeiro_produto')
export class ProdutoEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  nome: string;

  @Column({ nullable: true })
  descricao?: string;

  @Column('decimal', { precision: 10, scale: 2 })
  valor: number;

  @Column()
  ativo: boolean;

  @Column('timestamp')
  criadoEm: Date;
}

Repository Pattern

Generated (src/produto/repositories/produto.repository.ts):

import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { ProdutoEntity } from '../entities/produto.entity';

@Injectable()
export class ProdutoRepository extends Repository<ProdutoEntity> {
  constructor(private dataSource: DataSource) {
    super(ProdutoEntity, dataSource.createEntityManager());
  }

  async findAllActive(): Promise<ProdutoEntity[]> {
    return this.find({ where: { ativo: true } });
  }
}

Service Integration

Update service to use repository:

@Injectable()
export class ProdutoService {
  constructor(private readonly repository: ProdutoRepository) {}

  async findAll(): Promise<ProdutoEntity[]> {
    return this.repository.find();
  }

  async findOne(id: string): Promise<ProdutoEntity> {
    const produto = await this.repository.findOne({ where: { id } });
    if (!produto) {
      throw new NotFoundException(`Produto ${id} not found`);
    }
    return produto;
  }

  async create(createDto: CreateProdutoDto): Promise<ProdutoEntity> {
    return this.repository.save(createDto);
  }

  async update(id: string, updateDto: UpdateProdutoDto): Promise<ProdutoEntity> {
    await this.findOne(id);
    return this.repository.save({ id, ...updateDto });
  }

  async remove(id: string): Promise<void> {
    const result = await this.repository.delete(id);
    if (result.affected === 0) {
      throw new NotFoundException(`Produto ${id} not found`);
    }
  }
}

Database Configuration

Generate src/database.config.ts:

import { DataSourceOptions } from 'typeorm';

export const dataSourceOptions: DataSourceOptions = process.env.DATABASE_URL
  ? {
      type: 'postgres',
      url: process.env.DATABASE_URL,
      entities: ['dist/**/*.entity.js'],
      migrations: ['dist/migrations/*.js'],
      synchronize: false,
    }
  : {
      type: 'sqlite',
      database: 'dev.db',
      entities: ['dist/**/*.entity.js'],
      migrations: ['dist/migrations/*.js'],
      synchronize: true,
    };

Generate src/main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DataSource } from 'typeorm';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const dataSource = app.get(DataSource);

  if (!dataSource.isInitialized) {
    await dataSource.initialize();
    await dataSource.runMigrations();
  }

  await app.listen(3000);
}
bootstrap();

Type Mapping

Forge Type → TypeORM Column Type → SQL Type
string → varchar → VARCHAR
int → int → INTEGER  
float → float → FLOAT
decimal → decimal(10,2) → NUMERIC
boolean → boolean → BOOLEAN
uuid → uuid → UUID
datetime → timestamp → TIMESTAMP
date → date → DATE

Dependencies to Add

Update generated package.json:

{
  "dependencies": {
    "@nestjs/common": "^10.0.0",
    "@nestjs/core": "^10.0.0",
    "@nestjs/typeorm": "^9.0.0",
    "typeorm": "^0.3.0",
    "sqlite3": "^5.1.0"
  },
  "devDependencies": {
    "typeorm": "^0.3.0"
  }
}

Acceptance Criteria

  • Generated TypeORM entity matches Forge contract exactly
  • Entity decorators correct for all 8 primitive types
  • Repository extends TypeORM Repository
  • Service uses repository instead of in-memory array
  • All CRUD operations work with database
  • SQLite database persists across app restarts
  • PostgreSQL configuration documented
  • Generated code compiles without errors
  • Database queries actually execute against DB
  • Errors for missing/invalid records (404s)
  • Backward compatible: optional fields marked nullable
  • Performance: CRUD operations <100ms on local SQLite

Testing Strategy

Unit Tests

// tests/nest-typeorm.test.mjs

test('generated entity has correct decorators', () => {
  // Verify @Entity, @Column decorators present
})

test('repository extends TypeORM Repository', () => {
  // Verify ProdutoRepository extends Repository<ProdutoEntity>
})

test('service uses repository in findAll', () => {
  // Verify service calls this.repository.find()
})

Integration Tests

// tests/nest-database-integration.test.mjs

test('create produto persists to database', async () => {
  const app = await createTestApp();
  const response = await app.post('/produto').send({ 
    nome: 'Test Product', 
    valor: 99.99 
  });
  
  expect(response.status).toBe(201);
  const id = response.body.id;
  
  // Verify in database
  const db = app.get(DataSource);
  const saved = await db.getRepository(ProdutoEntity).findOne({ 
    where: { id } 
  });
  expect(saved.nome).toBe('Test Product');
})

test('update produto persists changes', async () => {
  // Create, update, verify in DB
})

test('delete removes from database', async () => {
  // Create, delete, verify gone from DB
})

test('restart preserves data in SQLite', async () => {
  // Create product
  // Stop app
  // Restart app
  // Verify product still exists
})

Database Tests

  • Start SQLite in-memory database
  • Run migrations
  • CRUD operations
  • Transaction handling
  • Error cases (404, constraint violations)

Files to Create/Modify

New Files:

  • packages/generators/src/nest/templates/entity.template.ts
  • packages/generators/src/nest/templates/repository.template.ts
  • packages/generators/src/nest/typeorm/column-mapper.ts
  • tests/nest-database-integration.test.mjs (200 lines)

Modified Files:

  • packages/generators/src/nest/generator.ts (+50 lines)
  • packages/generators/src/nest/templates/service.template.ts (full rewrite)
  • packages/generators/src/nest/templates/module.template.ts (+imports)

Non-Goals

  • Migrations from Forge contract changes
  • Query builder DSL
  • Query optimization
  • Relationship cascade operations
  • Soft deletes

Future Extensions

  • Automatic migrations from schema changes
  • Query logging and debugging
  • Connection pooling configuration
  • Read replicas support
  • Change data capture (CDC)
  • Audit trail (created_by, updated_by)

Dependencies

Blocks:

  • Feature 1.2.2: Complete CRUD Endpoints (depends on DB working)
  • Feature 1.2.3: Swagger Documentation (depends on working endpoints)
  • Feature 2.2.5: Relationship support (depends on base TypeORM setup)

Depends On:

  • Phase 0: Basic NestJS generation (✅ complete)

Implementation Notes

Column Type Precision

For decimal fields, use precision and scale:

@Column('decimal', { precision: 10, scale: 2 })
valor: number;

Date Handling

Map datetime → TIMESTAMP in SQL:

@Column('timestamp')
criadoEm: Date;

Entity Naming Convention

Table name: lowercase namespace + contract:

contract User → table: user
namespace auth, contract User → table: auth_user

Index Strategy

Always index primary key (auto). Future: add indexes on frequently queried fields.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions