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.
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).