Phase 3 · Databases & Persistent DataModule 20~64 min read

Repositories, Migrations & Data Access

Create stable persistence boundaries, evolve schemas with migrations, and test real database behavior without leaking SQL into routes.

What you'll learn

Persistence code must evolve without forcing routes and business rules to understand SQL history. Define repository contracts, version schema changes, seed repeatable data, and plan backward-compatible releases.

By the end of this lesson, you'll be able to:

  • Design repositories around use cases
  • Write ordered migrations
  • Test adapters against PostgreSQL
  • Sequence zero-downtime schema changes

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
RepositoryAn application-facing persistence portExpose domain operations, not a generic database wrapper
MigrationA versioned schema transitionKeep each change deterministic, reviewed, and deployed once
Expand-contractAdd compatibility before removing old shapeUse when application versions overlap

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Define the data-access boundary boundary: inputs, outputs, invariants, ownership, and expected failures.
  2. Design the data or message contract before choosing implementation details.
  3. Implement the smallest correct path with dependencies passed explicitly.
  4. Add validation, failure translation, cleanup, and concurrency behavior.
  5. Verify the boundary with realistic data and at least one adversarial case.
  6. Measure or observe the behavior before optimizing or extracting abstractions.

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Keep the contract domain-focused

The service depends on behavior while PostgreSQL stays an adapter detail.

task-service.js
export function createTaskService({ tasks, clock, ids }) {
  return {
    async create(ownerId, input) {
      const task = { id: ids.next(), ownerId, title: input.title, createdAt: clock.now() };
      return tasks.insert(task);
    },
  };
}

Expand before enforcing

Large live tables often need add, backfill, dual-write, and enforcement as separate releases.

003_priority.sql
ALTER TABLE tasks ADD COLUMN priority text;
-- Backfill in bounded batches; deploy writers; then enforce:
ALTER TABLE tasks ALTER COLUMN priority SET DEFAULT 'normal';
ALTER TABLE tasks ALTER COLUMN priority SET NOT NULL;

Production practice

Contract

Repository interfaces express domain behavior; migrations express stable production history.

Verification

Apply every migration from empty, upgrade an old snapshot, run repository contract tests, and verify rollback strategy.

Operations

Serialize migrators, back up high-risk changes, avoid long locks, and record the deployed schema version.

Common failure mode

Editing an applied migration creates different schemas with the same version. Add a new migration instead.

Independent workshop

Extract task persistence and introduce priorities without downtime.

Your finished workshop must include:

  • Repository interface
  • PostgreSQL adapter
  • Ordered migration
  • Deterministic seed
  • Contract tests
  • Deployment note

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • Repositories protect boundaries
  • Migrations are immutable history
  • Seeds are deterministic
  • Test the real engine
  • Compatibility spans releases

Quick check

1. What should a repository expose?

2. How do you correct an applied migration?

3. Why expand before contract?

Next: MongoDB & Document Modeling