Phase 5 · Node.js & Full StackModule 34~50 min read

Databases & Persistence

Persist application data safely with SQL, transactions, data access layers, and database tooling.

What you'll learn

Persistence code translates between domain operations and durable relational state. Parameterized queries, transactions, migrations, and small repository boundaries protect correctness.

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

  • Model relational data and constraints
  • Use parameterized queries
  • Define transaction and repository boundaries

Core mental model

Use this decision table as a compact reference. Focus on what each tool means and when it earns its place in production code.

ConceptWhat it meansDecision rule
ConstraintDatabase-enforced invariantEnforce uniqueness and relationships at the durable boundary
TransactionAtomic group of reads and writesUse when partial completion would violate an invariant
RepositoryDomain-focused persistence interfaceHide query mechanics, not domain decisions

Professional workflow

Build the behavior in small, observable steps. Each step should leave something you can inspect or test.

  1. Describe the persistence transaction boundary: inputs, outputs, state, timing, and expected failures.
  2. Implement the smallest correct path with names that expose intent.
  3. Add edge cases and failure handling before introducing abstractions.
  4. Verify behavior with realistic data and one deliberately adversarial example.
  5. Refactor only after the observable behavior is protected.

Make behavior observable

Before optimizing or abstracting, make inputs, outputs, state changes, timing, and failure paths visible. JavaScript becomes much easier to reason about when hidden work is exposed.

Guided code lab

Parameterize user input

The SQL structure stays separate from values, preventing injection.

course-repository.js
export async function findCourseBySlug(db, slug) {
  const result = await db.query(
    "SELECT id, slug, title FROM courses WHERE slug = $1",
    [slug],
  );
  return result.rows[0] ?? null;
}

Protect a multi-write invariant

Commit only after both updates succeed; otherwise rollback.

enrollment.js
await db.query("BEGIN");
try {
  await db.query("INSERT INTO enrollments(user_id, course_id) VALUES ($1, $2)", [userId, courseId]);
  await db.query("UPDATE courses SET seats = seats - 1 WHERE id = $1 AND seats > 0", [courseId]);
  await db.query("COMMIT");
} catch (error) {
  await db.query("ROLLBACK");
  throw error;
}

Production practice

Contract

Make the persistence transaction boundary explicit with validated inputs, structured outputs, owned resources, and stable failures.

Verification

Exercise normal work, invalid input, dependency failure, concurrency, and graceful cleanup in automated tests.

Operations

Use structured logs, health signals, timeouts, and configuration that can change without editing source code.

Common failure mode

String-concatenating SQL allows data to become executable query structure and creates injection vulnerabilities.

Independent workshop

Design persistence for courses, learners, and enrollments.

Your finished workshop must include:

  • Schema with keys and constraints
  • Parameterized repository methods
  • A transaction plus a rollback integration test

Definition of done

Demonstrate the happy path and at least two edge cases, keep responsibilities separated, and add a short note explaining one design choice.

Recap & quick check

Key takeaways

  • Constraints protect durable truth
  • Parameters separate code and data
  • Transactions make work atomic
  • Repositories expose domain-oriented persistence

Quick check

1. What prevents SQL injection most directly?

2. When is a transaction needed?

3. What should migrations be?

Keep the workshop. Later modules deliberately build on these decisions, so each exercise can become part of your final portfolio architecture.