CRUD endpoints are complete only when data persists and failure cases are handled deliberately.
Course navigationPhase 11⌄
Persistent Task CRUD API
Build persistent task CRUD through controller → service → Prisma with explicit contracts and verified database effects.
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.
Principles to keep while coding
Keep database calls behind service methods so controllers remain thin and testable.
Return the created or updated resource contract consistently after successful mutations.
Verify persistence independently with API requests and database inspection instead of trusting only the UI.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Define Task create/update/read response shapes before implementation.
- 02
Implement create through the service and verify both HTTP response and database row.
- 03
Implement list/read with deterministic ordering and missing-record behavior.
- 04
Implement partial update without overwriting omitted fields accidentally.
- 05
Implement delete semantics and decide whether the API returns the resource, metadata, or no content.
- 06
Run a manual CRUD script from empty database to cleanup and save it as a repeatable smoke test.
Failures you should know how to diagnose
Controllers contain Prisma calls
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
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.
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.
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.
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.
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
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
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
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
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
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
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 ↓
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
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
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
Guided Build Lab
Purpose Build persistent task CRUD through controller → service → Prisma with explicit contracts and verified database effects.
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.
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?
Mastery Capstone Lab
Integrate Phase 11 without following a transcript.
Exercises that increase independence
Define Task create/update/read response shapes before implementation.
Implement list/read with deterministic ordering and missing-record behavior.
reproduce and diagnose — Controllers contain Prisma calls.
deliver Working /tasks API supporting create/list/read/update/delete against PostgreSQL with consistent error behavior.
Can build CRUD without copy-paste controller logic and Can map persistence failures to API semantics.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
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
DONEreference/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",