Course navigationPhase 11
Volume 03 · Phase 11

Persistent Task CRUD API

Build persistent task CRUD through controller → service → Prisma with explicit contracts and verified database effects.

14lessons + labs
5klesson words
10source references
What you will ship

Phase outcome and project increment

Learning outcome

Build persistent task CRUD through controller → service → Prisma with explicit contracts and verified database effects.

Concrete deliverable

Working /tasks API supporting create/list/read/update/delete against PostgreSQL with consistent error behavior.

Figure 11.1 — Where this phase lives in TaskFlow
HTTP route
controller
service
Prisma query
PostgreSQL
mapped response
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

CRUD endpoints are complete only when data persists and failure cases are handled deliberately.

02

Keep database calls behind service methods so controllers remain thin and testable.

03

Return the created or updated resource contract consistently after successful mutations.

04

Verify persistence independently with API requests and database inspection instead of trusting only the UI.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Define Task create/update/read response shapes before implementation.

  2. 02

    Implement create through the service and verify both HTTP response and database row.

  3. 03

    Implement list/read with deterministic ordering and missing-record behavior.

  4. 04

    Implement partial update without overwriting omitted fields accidentally.

  5. 05

    Implement delete semantics and decide whether the API returns the resource, metadata, or no content.

  6. 06

    Run a manual CRUD script from empty database to cleanup and save it as a repeatable smoke test.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Controllers contain Prisma calls

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 2

PATCH treats omitted values as null/empty

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 3

Missing records bubble up as raw ORM exceptions

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 4

UI is considered proof without inspecting API/database

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Watch-order style

Lessons

The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.

01

From Memory to Durable State

From Memory to Durable State Why the storage lifetime must move beyond the Node.js process Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone In-memo

2 min
02

Schema Migration and Development Seed

Schema Migration and Development Seed Evolve additively, generate safely, seed the minimum relational graph Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture

2 min
03

Tasks Module and Service Architecture

Tasks Module and Service Architecture Controllers, services, dependency injection, and a clean Prisma boundary Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architectu

2 min
04

Create Persistent Tasks

Create Persistent Tasks POST /tasks from HTTP request to PostgreSQL INSERT Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone POST /tasks ↓ Project r

2 min
05

Read Persistent Tasks

Read Persistent Tasks Collection and detail queries with meaningful not-found behavior Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone GET /tasks

2 min
06

Update Persistent Tasks

Update Persistent Tasks PATCH semantics, Prisma update, and P2025 translation Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone PATCH /tasks/:id ↓ P

2 min
07

Delete and Error Semantics

Delete and Error Semantics 204 deletion, missing rows, and honest HTTP meaning Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone DELETE /tasks/:id ↓

2 min
08

Persistence Verification and Database Inspection

Persistence Verification and Database Inspection Prove durability with restart tests, Prisma Studio, and SQL Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture

2 min
09

End to End Persistent TaskFlow

End-to-End Persistent TaskFlow Reconnect the thin Next.js view and debug across layers Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone Next.js Ser

2 min
10

Hands On Lab and Phase Exit

Hands-On Lab and Phase Exit Follow, modify, implement, engineer, and prove Phase 11 understanding Phase 11 - Persistent Task CRUD API Volume 03 - Database Engineering Architecture milestone

2 min
LAB

Guided Build Lab

Purpose Build persistent task CRUD through controller → service → Prisma with explicit contracts and verified database effects.

3 min
LAB

Debugging Workshop

Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.

2 min
LAB

Phase Synthesis & Independent Challenge

Mission Rebuild the capability of Phase 11 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 11 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Define Task create/update/read response shapes before implementation.

Practice

Implement list/read with deterministic ordering and missing-record behavior.

Debug

reproduce and diagnose — Controllers contain Prisma calls.

Independent

deliver Working /tasks API supporting create/list/read/update/delete against PostgreSQL with consistent error behavior.

Explain-back

Can build CRUD without copy-paste controller logic and Can map persistence failures to API semantics.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 11's engineering goal?
02Which practice best matches Phase 11's engineering goal?
03Which practice best matches Phase 11's engineering goal?
04Which practice best matches Phase 11's engineering goal?
05You are working through the Phase 11 lab. Which next action is the most disciplined step?
06You are working through the Phase 11 lab. Which next action is the most disciplined step?
07You are working through the Phase 11 lab. Which next action is the most disciplined step?
08You are working through the Phase 11 lab. Which next action is the most disciplined step?
Source snapshot

Reference files

Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.

reference/TaskFlow_Persistent_CRUD_Snapshot/README.txt
TaskFlow Phase 11 reference snapshot

This snapshot demonstrates the phase-end shape only.
Do not copy generated Prisma Client files or migration timestamps from documentation.
Generate them from your real schema and database state.

Run from apps/api:
  pnpm exec prisma validate
  pnpm exec prisma migrate dev --name add_taskflow_domain
  pnpm exec prisma generate
  pnpm exec ts-node prisma/seed.ts
  pnpm run build
  pnpm run start:dev
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/prisma/schema.prisma
generator client {
  provider     = "prisma-client"
  output       = "../src/generated/prisma"
  moduleFormat = "cjs"
}

datasource db {
  provider = "postgresql"
}

enum MembershipRole {
  OWNER
  ADMIN
  MEMBER
}

enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/prisma/seed.ts
import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import {
  MembershipRole,
  PrismaClient,
} from "../src/generated/prisma/client";

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
  throw new Error("DATABASE_URL is required before running the seed.");
}

const prisma = new PrismaClient({
  adapter: new PrismaPg({ connectionString }),
});

async function main() {
  const user = await prisma.user.upsert({
    where: { email: "learner@taskflow.local" },
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/src/app.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { HealthModule } from "./health/health.module";
import { PrismaModule } from "./prisma/prisma.module";
import { TasksModule } from "./tasks/tasks.module";

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    PrismaModule,
    HealthModule,
    TasksModule,
  ],
})
export class AppModule {}
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/src/tasks/tasks.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  HttpStatus,
  Param,
  Patch,
  Post,
} from "@nestjs/common";
import { TasksService } from "./tasks.service";
import {
  CreateTaskInput,
  UpdateTaskInput,
} from "./tasks.types";

@Controller("tasks")
export class TasksController {
  constructor(private readonly tasksService: TasksService) {}
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/src/tasks/tasks.module.ts
import { Module } from "@nestjs/common";
import { TasksController } from "./tasks.controller";
import { TasksService } from "./tasks.service";

@Module({
  controllers: [TasksController],
  providers: [TasksService],
})
export class TasksModule {}
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/src/tasks/tasks.service.ts
import {
  Injectable,
  NotFoundException,
} from "@nestjs/common";
import { Prisma } from "../generated/prisma/client";
import { PrismaService } from "../prisma/prisma.service";
import {
  CreateTaskInput,
  UpdateTaskInput,
} from "./tasks.types";

@Injectable()
export class TasksService {
  constructor(private readonly prisma: PrismaService) {}

  findAll() {
    return this.prisma.task.findMany({
      orderBy: { createdAt: "desc" },
    });
  }
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/api/src/tasks/tasks.types.ts
import {
  TaskPriority,
  TaskStatus,
} from "../generated/prisma/client";

// These are compile-time shapes only. Phase 12 replaces this temporary
// boundary with DTOs + runtime validation.
export type CreateTaskInput = {
  projectId: string;
  title: string;
  description?: string | null;
};

export type UpdateTaskInput = {
  title?: string;
  description?: string | null;
  status?: TaskStatus;
  priority?: TaskPriority;
};
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/web/src/app/tasks/page.tsx
import { getTasks } from "@/lib/api/tasks";

export default async function TasksPage() {
  const tasks = await getTasks();

  return (
    <main>
      <h1>Tasks</h1>

      {tasks.length === 0 ? (
        <p>No persisted tasks yet.</p>
      ) : (
        <ul>
          {tasks.map((task) => (
            <li key={task.id}>
              <strong>{task.title}</strong>
              {" - "}
              {task.status}
            </li>
          ))}
reference/TaskFlow_Persistent_CRUD_Snapshot/apps/web/src/lib/api/tasks.ts
export type Task = {
  id: string;
  projectId: string;
  title: string;
  description: string | null;
  status: "TODO" | "IN_PROGRESS" | "DONE" | "CANCELED";
  priority: "LOW" | "MEDIUM" | "HIGH" | "URGENT";
  createdAt: string;
  updatedAt: string;
};

const API_BASE_URL = process.env.API_BASE_URL;

if (!API_BASE_URL) {
  throw new Error("API_BASE_URL is required.");
}

export async function getTasks(): Promise<Task[]> {
  const response = await fetch(`${API_BASE_URL}/tasks`, {
    cache: "no-store",