Capstone: Building a Full-Stack Task Manager

Advanced
16 min

Capstone: Building a Full-Stack Task Manager

This final chapter assembles the course into one application: a multi-user task manager with authentication, a database, Server Actions, optimistic UI, tests and a production build. It shows the skeleton and the decisions behind each piece. After this lesson you will be able to start a full-stack Next.js project with a structure that scales.

Scope and Stack

Features: sign up and log in, a private task list with instant create and toggle, a public landing page, SEO metadata, tests and a Docker image.

| Concern | Choice | Lesson | |---|---|---| | Database | Drizzle with SQLite locally, PostgreSQL in production | Database Access | | Auth | jose session cookie verified in a DAL | Authentication Patterns | | Mutations | Server Actions, Zod, useActionState, useOptimistic | Server Actions, Form State | | Tests | Vitest for the DAL, Playwright for flows | Testing | | Deploy | output: "standalone" and a Dockerfile | Docker and Self-Hosting |

Project Structure

text
src/ ├── app/ │ ├── (marketing)/page.tsx # public landing page │ ├── (auth)/login/page.tsx # login form + action │ ├── (app)/ │ │ ├── layout.tsx # verifySession(), nav │ │ └── tasks/ │ │ ├── page.tsx # loads tasks (server) │ │ ├── actions.ts # createTask, toggleTask │ │ ├── TaskForm.tsx # useActionState │ │ ├── TaskList.tsx # useOptimistic │ │ └── loading.tsx, error.tsx │ └── layout.tsx # root layout, fonts, metadata ├── db/ (schema.ts, index.ts) ├── lib/ (dal.ts, session.ts, env.ts) └── proxy.ts # redirects anonymous users from /tasks

Route groups separate the three shells, and all data access goes through lib/dal.ts.

Schema and Data Access

typescript
// src/db/schema.ts import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; import { users } from "./users"; // id, email, passwordHash export const tasks = sqliteTable("tasks", { id: text("id").primaryKey(), userId: text("user_id").notNull().references(() => users.id), title: text("title").notNull(), done: integer("done", { mode: "boolean" }).notNull().default(false), });

src/db/index.ts exports db from drizzle with the better-sqlite3 driver; npx drizzle-kit push creates the tables. The DAL function getTasks() calls verifySession() first and filters by userId, as do the actions in the sample at the top of this lesson, so cross-user access is impossible by construction.

The Page and Its Client Components

tsx
// src/app/(app)/tasks/page.tsx import { getTasks } from "@/lib/dal"; import { TaskForm } from "./TaskForm"; import { TaskList } from "./TaskList"; export default async function TasksPage() { const tasks = await getTasks(); return ( <main> <h1>Tasks</h1> <TaskForm /> <TaskList tasks={tasks} /> </main> ); }

TaskForm wires createTask through useActionState and shows the returned error. TaskList wraps toggleTask in useOptimistic so a checkbox flips instantly and reverts if the action fails. loading.tsx shows a skeleton and error.tsx offers a retry. The page is a Server Component, so the query never reaches the browser.

Hardening and Shipping

  • Proxy: redirects /tasks to /login when the session cookie is absent; the DAL remains the real check.
  • Environment: lib/env.ts validates DATABASE_URL and SESSION_SECRET with Zod.
  • SEO: the landing page exports metadata; sitemap.ts lists public routes and robots.ts disallows /tasks.
  • Tests: Vitest covers getTasks scoping with a mocked session; Playwright adds a task and asserts it persists.
  • Build: next build should show the landing page as static and /tasks as dynamic. Ship with the standalone Dockerfile and point DATABASE_URL at PostgreSQL.
Quick Quiz
Question 1 of 3

Where does the capstone enforce that users only see their own tasks?

Key Takeaways

  • Route groups, colocated actions and a single DAL give a structure that scales.
  • Server Components load data; small Client Components add interactivity.
  • Every query and action verifies the session and scopes by user; the proxy only improves the experience.
  • Validation, metadata, loading and error states are part of the minimum.
  • Check rendering modes in the build output, test at two levels, and ship as a standalone container.

Next lesson: What to learn next — deepen React 19 (Suspense, transitions, the Compiler), study relational database design, and set up CI/CD so every push runs your tests and builds.

Capstone: Building a Full-Stack Task Manager - Next.js | CodeYourCraft | CodeYourCraft