Fullstack CourseLearn by building
Back to week 2

Topic

Nest ↔ PostgreSQL: TypeORM root/env

Definition

TypeOrmModule.forRoot registers a single application-wide Postgres connection (and connection pool) for the lifetime of the Nest application, configured from environment values rather than hardcoded settings.

In simpler words

One line in AppModule tells Nest which Postgres to talk to, using the same DATABASE_URL from the SQL connection topic — nothing new, just wired into Nest.

This is where the SQL-level concepts (URL, entities-as-tables, no synchronize) become one concrete Nest config object.

After this you can

  • Read a typical forRoot() call in a Nest AppModule
  • Explain where env.DATABASE_URL comes from before it reaches forRoot
  • Say why entities and migrations arrays are both listed

A typical forRoot() config

Definition

TypeOrmModule.forRoot accepts a connection configuration object (driver type, URL, entity classes, migration behavior) and Nest uses it once to open the pooled connection for the whole app.

In simpler words

Nest ≠ Next — this connection lives in the API process only; the Next.js web app never talks to Postgres directly.

type: 'postgres' selects the driver; url: env.DATABASE_URL supplies everything covered in the previous topic.

entities: [User, Ticket] tells TypeORM which classes describe the schema it should expect.

synchronize: false is non-negotiable here — schema changes only ever come from committed migrations.

AppModule config

TypeOrmModule.forRoot({
  type: 'postgres',
  url: env.DATABASE_URL,
  entities: [User, Ticket],
  // Realtime rule: migrations own the schema. Never true in shared/staging/prod.
  synchronize: false,
  migrations: ['dist/database/migrations/*.js'],
  migrationsRun: false,
}),

migrationsRun: false means migrations are run explicitly (pnpm db:migration:run), not automatically on boot.

DATABASE_URL travels through a shared env schema

Definition

A shared, validated environment schema parses process.env once and produces a typed object, so every consumer (Nest app, TypeORM CLI) reads the same guaranteed-valid values.

In simpler words

AppModule never reads process.env.DATABASE_URL directly — it reads env.DATABASE_URL after loadApiEnv() has validated it.

A shared env schema (for example apiEnvSchema) requires DATABASE_URL as a non-empty string — a missing or empty value fails fast at boot instead of failing confusingly later.

The TypeORM CLI (data-source.ts) reads the same variable independently via dotenv, because CLI commands run outside the Nest bootstrap.

Two readers, one variable

// app.module.ts (Nest runtime)
const env = loadApiEnv(); // validated
url: env.DATABASE_URL

// database/data-source.ts (TypeORM CLI)
loadDotenv({ path: resolve(__dirname, '../../.env') });
const url = process.env.DATABASE_URL;
if (!url) throw new Error('DATABASE_URL is required for TypeORM CLI');

Same .env, two entry points — the CLI cannot boot Nest just to run a migration.

Why both entities and migrations are configured

Definition

The entities array tells the running application which TypeScript classes map to tables for querying, while the migrations array (with migrationsRun) controls whether TypeORM applies pending schema changes automatically at startup.

In simpler words

They answer two different questions: “what shape do I expect the data in?” versus “should I change the schema right now?”

A disciplined setup answers the second question with migrationsRun: false — migrations run as a deliberate CLI step, never as a side effect of starting the API.

The CLI’s data-source.ts points migrations at TypeScript source (for generate/run during development); AppModule points at compiled dist/*.js (for what actually executes at runtime).

CLI vs runtime migration paths

// data-source.ts (CLI, source files)
migrations: [resolve(__dirname, './migrations/*.{ts,js}')]

// app.module.ts (runtime, compiled output)
migrations: ['dist/database/migrations/*.js']

The CLI works against source during development; the deployed app only ever sees compiled JS.

Keep in mind

  • Trace every config value back to its source before debugging further — most “TypeORM” bugs are actually env bugs.
  • synchronize: false + migrationsRun: false means: nothing about the schema changes without someone running a command on purpose.
  • Nest ≠ Next: only the API process holds this connection.

Test

Check your understanding

At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.

Checking your session…

15 questions · concept 5 · syntax 3 · practical 4 · logic 3

1. What does TypeOrmModule.forRoot do?
Concept
2. Where does this academy typically pass the DB URL?
Syntax
3. What is synchronize:false for?
Practical
4. What does TypeOrmModule.forFeature do?
Logic
5. Which app should own TypeORM in this course?
Concept
6. What should entities listed in forRoot include?
Practical
7. Can forRoot and data-source.ts drift?
Syntax
8. What happens if DATABASE_URL is wrong at boot?
Logic
9. Is synchronize:true a substitute for reviewed migrations in shared envs?
Concept
10. Which statement about pooling with TypeORM is true?
Practical
11. Why is this TypeORM config dangerous for a shared/production database?
Conceptadvanced
TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  synchronize: true,
});
12. TicketsService cannot inject Repository<Ticket>. Which import fixes the module?
Syntaxintermediate
@Module({
  imports: [/* ? */],
  providers: [TicketsService],
})
export class TicketsModule {}
13. What does this configuration establish?
Practicalintermediate
TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [Ticket, User],
  synchronize: false,
});
14. What breaks with an empty entities list (and no autoLoadEntities)?
Logicadvanced
TypeOrmModule.forRoot({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  entities: [],
});
15. What does extra.max configure here?
Conceptintermediate
TypeOrmModule.forRoot({
  type: 'postgres',
  url,
  extra: { max: 10 },
});

Checking your session…