Part 3.6 — Migrations, Environments & Local Dev Workflows
One thing separates hobby projects from production-ready systems: disciplined environment and migration management.
The moment you work with teammates or deploy remotely, you need:
predictable environments
safe migrations
reproducible PostgreSQL setups
dev/preview/production database separation
automation for local workflows
This post guides you through building a clean, well-structured workflow you’ll use throughout the rest of this series.
1. Environment Files (.env) — Keep Secrets Out of Git
Every NestJS + Prisma backend needs separate environment files:
.env # local
.env.development # local explicit
.env.staging # preview
.env.production # productionEach file defines at least:
DATABASE_URL="postgresql://postgres:password@localhost:5432/fullstack_db"
PORT=3000
NODE_ENV=developmentBest practices:
Never commit
.envProvide
.env.examplewith placeholdersUse secrets in CI/CD (GitHub Actions → Encrypted Secrets)
Validate required env vars on startup (we’ll do this in Part 4 & 9)
2. Docker-Based Local PostgreSQL (No Local Installs Needed)
Your local DB should run in Docker so teammates can reproduce your setup instantly.
docker-compose.yml:
services:
db:
image: postgres:15
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: fullstack_db
volumes:
- dev_pg:/var/lib/postgresql/data
volumes:
dev_pg:Start it:
docker compose up -dNow everyone has the same DB engine, same version, same config.
3. Running Prisma Against Your Local DB
Every time you change the schema:
pnpm prisma migrate dev --name descriptive_nameExample:
pnpm prisma migrate dev --name add_user_profileThis will:
generate SQL migrations
apply them to your local DB
update Prisma Client types
4. Resetting the Local Database (Useful for Dev)
pnpm prisma migrate resetThis will:
wipe all data
re-run all migrations
optionally re-run seed scripts
Perfect for testing evolving schemas.
5. Writing Seed Scripts (Populate Fake Data)
File: prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@example.com',
posts: {
create: [
{ title: 'Hello World', body: 'My first post' },
{ title: 'React + NestJS', body: 'Full stack is fun!' },
],
},
},
});
}
main()
.catch(e => {
console.error(e);
process.exit(1);
})
.finally(() => prisma.$disconnect());Enable seeding in package.json:
"prisma": {
"seed": "ts-node prisma/seed.ts"
}Run:
pnpm prisma db seedReact frontend development becomes easier with sample data.
6. Managing Multiple Databases (Dev, Staging, Prod)
You must separate environments:
Local / Dev
DATABASE_URL="postgresql://postgres:password@localhost:5432/fullstack_dev"Staging
DATABASE_URL="${{ secrets.STAGING_DATABASE_URL }}"Production
DATABASE_URL="${{ secrets.PROD_DATABASE_URL }}"Your backend must NOT talk to production locally — one wrong migration can destroy data.
7. How CI/CD Runs Migrations (Important)
In GitHub Actions (Part 11), you’ll need something like:
- name: Apply migrations
run: pnpm prisma migrate deployNever use migrate dev in CI or production.
Use:
pnpm prisma migrate deployThis applies generated migrations without generating new ones.
This matters for:
schema stability
predictable production state
safe rollouts
8. Using Preview Databases for Feature Branches
Platforms like Render, Fly.io, and Neon support ephemeral DBs.
Workflow:
Create preview environment per PR
Migrate preview DB
Deploy preview API
Frontend points to it via VITE_API_URL
Your team can test features with real data before merging.
9. Auto-Generating Prisma Clients in CI
Whenever your schema changes:
GitHub Actions should run:
- name: Generate Prisma Client
run: pnpm prisma generateThis ensures:
TypeScript builds correctly
NestJS services have updated types
No breaking migrations sneak into PRs
10. Making .env Available in NestJS
NestJS uses @nestjs/config:
pnpm add @nestjs/configIn app.module.ts:
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env', `.env.${process.env.NODE_ENV}`],
});Usage:
constructor(private config: ConfigService) {}
const dbUrl = this.config.get<string>('DATABASE_URL');This abstracts environment management neatly.
11. Creating a Reproducible Local Dev Script
Improve DX with pnpm scripts:
"scripts": {
"dev": "nest start --watch",
"db:start": "docker compose up -d",
"db:stop": "docker compose down",
"migrate": "prisma migrate dev",
"seed": "prisma db seed"
}Add these for smoother workflows.
12. Versioning Migrations and Schema Changes
Rules:
never edit old migrations
if schema changes → create new migration
treat migrations as history
review every SQL file
require PR approval for schema updates (important in teams)
A clean migration history equals painless deployments.
13. Handling Breaking Database Changes Safely
When changing columns or relations:
plan additive → then destructive
migrate with null defaults → then backfill → then remove defaults
never drop columns without a migration safety window
use feature flags
PostgreSQL is unforgiving; structure changes must be deliberate.
14. Summary
You now understand:
how to configure portable
.envsetupshow to use Dockerized PostgreSQL
how Prisma migrations work in dev
how migrations are applied in CI/CD
how to seed local databases
how to manage dev/staging/prod environments safely
how to build workflows that scale with your team
This completes Part 3 — Backend Foundations with NestJS + Prisma.
Next up:
➡️ Create Series for Part 4 — Testing the Backend: Jest, Supertest, and Auth Guards
Related
Part 4.5 — Database Reset, Transactions & Running Tests in CI
Reliable backend tests require strict database isolation and disciplined CI workflows. This post shows how to reset PostgreSQL safely, use transactions, and run tests confidently in automation.
Database Design Fundamentals: Schema Evolution and Migrations Explained
Learn how databases evolve with schema changes. Understand migrations, versioning, and best practices for adapting databases over time.
Database Design Fundamentals: Many-to-Many Relationships and Join Tables
Understand many-to-many relationships in databases and how join tables solve them with clear examples and best practices.
Comments