Skip to content

Latest commit

 

History

86 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Acme Dashboard

Live Demo Next.js React TypeScript Drizzle ORM Auth.js Vitest Tailwind CSS MIT License

A full-stack invoice and customer management application built with Next.js, React 19, and TypeScript. The project uses Server Actions for mutations, Drizzle ORM for type-safe database access, Auth.js for authentication, and Vitest for component testing — with a focus on accessibility, reusable abstractions, and modern React patterns like useActionState and useFormStatus.

Acme Dashboard Demo

Live Demo

A working deployment showcasing authentication, CRUD flows, validation, flash notifications, loading states, and accessibility features is available at acme.ygbstudio.net.

Demo Credentials:

  • Email: user@nextmail.com
  • Password: 123456

This project originated from the official Next.js App Router tutorial. After completing the guided portion, the repository became a deliberate engineering exercise — an opportunity to work through the kinds of problems that tutorials intentionally skip: migrating a data layer, designing reusable components, writing meaningful tests, building accessible interactions, and maintaining a codebase as it evolves through incremental refactoring. The commit history reflects that iterative process rather than a single large implementation.

Why This Exists

The Next.js tutorial teaches the App Router's fundamentals well, but it stops short of the engineering concerns that arise in real applications — testing, architectural refactoring, accessibility, reusable component design, and data layer maintainability.

This repository exists to explore those concerns. It is not intended as a production application, but as a working codebase where each technical decision was made to solve a specific problem and can be examined in context.


Tech Stack

Layer Technology
Framework Next.js (App Router), React 19
Language TypeScript 5.7 (strict)
Styling Tailwind CSS
Database PostgreSQL via Neon
ORM Drizzle ORM
Authentication Auth.js v5 (Credentials provider)
Validation Zod
Testing Vitest, React Testing Library, jsdom

Engineering Highlights

The following areas represent independent engineering work performed after completing the guided tutorial:

  • 75 tests across 10 files — validating form interaction, async submission, pending UI states, accessibility attributes, toast lifecycle, dialog keyboard behavior, and pagination boundaries
  • ORM migration — replaced raw SQL with Drizzle ORM; schema, queries, and TypeScript types are co-derived and verified at compile time
  • Customer CRUD — the tutorial's customer page is read-only; all create, edit, and delete workflows are original
  • React 19 patternsuseActionState for form state management, useFormStatus for pending UI, and Server Actions as the mutation layer
  • 25+ reusable components and 3 custom hooks in app/ui/common/ — table primitives, buttons, dialogs, error blocks, form shells, pagination — consumed by both invoices and customers
  • Flash notifications — cookie-based system using UUID event IDs to show exactly one toast per mutation, surviving redirects and re-renders
  • Accessibility as design — ARIA live regions, keyboard-navigable dialogs, screen-reader labels, and semantic navigation throughout
  • Type-level schema enforcement — compile-time Expect<Equal<>> assertions verify that hand-written types match Drizzle's inferred schema types

Features

  • Credential-based authentication with session-protected routes
  • Dashboard overview with revenue chart, summary cards, and latest invoices
  • Full CRUD for invoices: create, edit, delete with search and pagination
  • Full CRUD for customers: create, edit, delete with search and pagination
  • Flash notifications for every successful or failed mutation
  • Confirmation dialog before destructive actions
  • Loading skeletons, error boundaries, and accessible breadcrumbs
  • Configurable latency simulation for demonstrating Suspense loading states

Architecture

graph LR
    A[Browser] --> B[Server Components]
    A --> C[Server Actions]
    C --> D[Zod Validation]
    D --> E[Drizzle ORM]
    B --> E
    E --> F[(PostgreSQL)]
    C --> G[Flash Cookies]
    G --> B
Loading
sequenceDiagram
    participant Browser
    participant Middleware as Middleware (Edge)
    participant Server as Next.js Server (Node)
    participant DB as PostgreSQL

    Browser->>Middleware: Request /dashboard/*
    Middleware->>Middleware: auth.config.ts — authorized()
    alt Not authenticated
        Middleware-->>Browser: Redirect → /login
    else Authenticated
        Middleware->>Server: Forward request
        Server-->>Browser: Render Server Component
    end

    Browser->>Server: POST /login (credentials form)
    Server->>Server: Zod validates input
    Server->>DB: SELECT user by email
    DB-->>Server: User record
    Server->>Server: bcryptjs.compare(password, hash)
    Server-->>Browser: Session cookie
Loading

Server Components fetch data directly via Drizzle. Mutations flow through Server Actions that validate input with Zod, write to the database, set a flash cookie, and redirect. Client Components manage interactivity — form state via useActionState, pending indicators via useFormStatus, toast visibility, dialog toggling — without a dedicated state management library.

Authentication is split across two files by design: auth.config.ts runs in the Edge runtime (no Node.js APIs, no database calls) and gates every dashboard request at the middleware layer. The full auth.ts runs only in the Node.js runtime when credentials are actually submitted.


Design Principles

  • Composition over duplication — shared UI primitives are extracted once and consumed by both feature domains
  • Test observable behavior, not implementation — the suite validates what users see and interact with, not internal component state
  • Validate at the server boundary — every Server Action validates its input with Zod before touching the database
  • Accessibility is a first-class design constraint — ARIA attributes and keyboard behavior are part of component design from the start, not applied retroactively
  • Use TypeScript to enforce architecture — compile-time type assertions catch schema drift before it becomes a runtime error

Engineering Beyond the Tutorial

Data Layer

The tutorial queries the database with raw SQL template literals. This project replaces the entire data layer with Drizzle ORM, introducing a declarative schema (app/lib/db/schema.ts) that serves as the single source of truth for table structure, foreign keys, and constraints. TypeScript types are derived from the schema at compile time, and app/lib/definitions.ts uses Expect<Equal<>> assertions to verify that hand-written types stay in sync with Drizzle's inferred types.

Authentication

Auth.js v5 handles session management with a Credentials provider. Input is validated with Zod before querying the database; passwords are verified with bcryptjs. Route protection runs at the Edge middleware layer, keeping the database out of the request path for unauthenticated visitors. Sign-in and sign-out are exposed as Server Actions callable from forms.

Customer Management

The tutorial leaves the customer page read-only. All mutation functionality is original: routes under app/dashboard/customers/, Zod-validated Server Actions with field-level error returns, auto-generated avatars on creation, and a guard that prevents deletion of customers with associated invoices.

Flash Notifications

The tutorial provides no feedback after mutations. This project adds a cookie-based flash system: after a Server Action completes, three short-lived cookies (variant, message, UUID event ID) are written. On the next render, TrackedToastNotification reads them and activates once per event ID, ensuring each notification appears exactly once regardless of re-renders.

Reusable UI Components

Shared primitives live in app/ui/common/ rather than being duplicated per feature. Table components are consumed identically by invoices and customers. Generic button, dialog, form shell, hook, and error components are composed into feature-specific views, keeping domain components focused on their own logic.

Destructive Action Confirmation

Delete operations open a modal dialog rather than executing immediately. The dialog defaults keyboard focus to the cancel button, closes on Escape or backdrop click, shows a spinner during the pending state, and triggers the Server Action through a wrapping form on confirmation.

Developer Experience

  • Path aliases (@lib/*, @ui/*, @app/*) reduce import depth across the project
  • Demo latency mode introduces configurable artificial delay to make Suspense skeletons visible during demonstrations
  • Prettier with Tailwind plugin enforces consistent class ordering

Accessibility

Accessibility is treated as part of component design rather than a separate compliance pass:

  • Validation errors are announced via ARIA live regions without interrupting screen-reader speech
  • Breadcrumbs use semantic <nav> with aria-current="page" on the active item
  • The confirmation dialog uses role="dialog" with aria-modal, labeled regions, and keyboard focus defaulting to the safe action
  • Toast notifications distinguish between role="status" (success) and role="alert" (error)
  • Destructive and edit action buttons carry screen-reader-only labels
  • Escape closes the dialog; Enter activates the focused action

Testing

The project includes 75 tests across 10 files with 84.2% statement coverage, 86.8% branch coverage, 89.5% function coverage, and 86.8% line coverage (V8).

Metric Coverage
Statements 84.2%
Branches 86.8%
Functions 89.5%
Lines 86.8%

Coverage is intentionally focused on the application's behavior rather than achieving an arbitrary percentage. The highest coverage is concentrated on reusable UI components, custom hooks, accessibility behavior, form interactions, asynchronous state transitions, and pending UI—areas where regressions would have the greatest impact on users. Utility helpers are tested selectively based on their complexity and business value rather than solely to increase coverage metrics.

The test suite uses Vitest, React Testing Library, and jsdom, focusing on observable user behavior rather than implementation details. The suite validates what a user would see and do: interacting with form fields, submitting with valid and invalid data, observing validation errors, watching buttons enter pending states during async operations, dismissing toasts, and navigating dialogs with keyboard and mouse. It also verifies that accessibility attributes — live regions, ARIA roles, screen-reader announcements — are correctly applied where they matter.

pnpm test              # watch mode
pnpm test:coverage     # v8 coverage report

Lessons Learned

  • Migrating from raw SQL to Drizzle reinforced the value of compile-time guarantees. Once the schema becomes the single source of truth, an entire class of bugs — column name mismatches, incorrect field types, missing constraints — becomes impossible to ship silently.
  • Testing asynchronous UI and pending states required a deeper understanding of React's rendering model. Fake timers, act() boundaries, and the order of state updates matter in ways that are easy to get wrong.
  • Accessibility is easier when designed into reusable components than when added afterward. A dialog that handles Escape, focus management, and ARIA attributes once is significantly simpler than applying those same concerns to every individual instance.
  • Server Actions require different testing strategies than traditional client-side APIs. Mocking at the module level is straightforward, but testing actions themselves in isolation — without a database — is a gap the current suite does not fully address.
  • Small, composable primitives reduce duplication more effectively than well-documented large components. The table primitives in app/ui/common/ are simple enough that developers can read them in seconds, yet flexible enough to cover both invoices and customers without modification.

Project Structure

app/
├── api/auth/           — Auth.js route handlers
├── dashboard/
│   ├── (overview)/     — Dashboard home (route group)
│   ├── customers/      — Customer list, create, edit routes
│   └── invoices/       — Invoice list, create, edit routes
├── lib/
│   ├── actions/        — Server Actions (auth, cookies, customers, invoices)
│   ├── db/             — Drizzle schema and relations
│   ├── data.ts         — Data-fetching functions
│   ├── definitions.ts  — Types, Zod schemas, compile-time assertions
│   └── utils.ts        — Formatting, pagination, avatar, latency helpers
├── login/
├── seed/
└── ui/
    ├── common/         — Shared primitives
    ├── customers/      — Customer-specific forms and table
    ├── dashboard/      — Charts and cards
    └── invoices/       — Invoice-specific forms and table
auth.config.ts          — NextAuth middleware config (Edge)
auth.ts                 — NextAuth config with database access
drizzle.config.ts       — Drizzle Kit configuration
vitest.config.ts
vitest.setup.ts

Getting Started

Prerequisites: Node.js 18+, pnpm, a PostgreSQL database (Neon or compatible)

git clone https://github.com/Urbine/acme-dashboard.git
cd acme-dashboard
pnpm install
cp .env.example .env
Variable Required Description
DATABASE_URL Yes Pooled PostgreSQL connection string
DATABASE_URL_UNPOOLED Yes Direct connection string for Drizzle Kit
AUTH_SECRET Yes Session signing key — generate with openssl rand -base64 32
SEED_TOKEN Yes Token used to protect seed route - generate with openssl rand -hex 32
DEMO_LATENCY No Set to "true" for artificial latency in Suspense demos
DEMO_MODE No Set to "true" to enable schema destruction for demo seeding
pnpm dev           # development server (Turbopack)
pnpm build         # production build
pnpm lint          # ESLint
pnpm format        # Prettier
pnpm test          # Vitest watch
pnpm test:coverage # coverage report

Seed the database by visiting http://localhost:3000/seed?token=<SEED_TOKEN> after starting the dev server.

Note: Without a matching SEED_TOKEN, the route responds with HTTP 403.


Upstream Contribution

While extending and testing this project, a concurrency issue was identified in the official tutorial's database seed implementation. The investigation led to an upstream contribution:

vercel/next-learn#1323 — fix: resolve duplicate extension race condition in seed route

The tutorial's seed route ran CREATE EXTENSION IF NOT EXISTS "uuid-ossp" inside a sql.begin transaction block. Under parallel execution, concurrent transactions raced to write to pg_catalog.pg_extension, triggering a PostgreSQL unique_violation error (code 23505). The fix moves extension creation outside the transaction so it runs once before seeding begins.


Future Improvements

  • E2E tests — the current suite covers units and components; end-to-end user flows are not yet covered
  • Server Action tests — actions are tested only indirectly through form component mocks
  • Image upload — customer avatars are auto-generated; file upload would require object storage
  • Role-based access — the auth model could support multiple permission levels
  • Optimistic updates — mutations currently wait for the server; optimistic patterns would reduce perceived latency

License

This project is licensed under the MIT License. See the LICENSE file for details.


Credits


Closing Thoughts

The repository documents an incremental engineering process: architectural decisions, refactorings, testing additions, and accessibility improvements introduced over time to solve concrete problems. Each section of the codebase reflects a specific engineering concern, and the commit history traces how the project arrived at its current shape.

About

Full-stack Next.js application demonstrating modern React patterns, Server Actions, Drizzle ORM, authentication, testing, and accessible component design.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages