Course navigationPhase 06
Volume 02 · Phase 06

Next.js <-> NestJS Communication

Connect Next.js and NestJS through an explicit, debuggable HTTP boundary with environment-safe base URLs and correct CORS behavior.

13lessons + labs
7klesson words
21source references
What you will ship

Phase outcome and project increment

Learning outcome

Connect Next.js and NestJS through an explicit, debuggable HTTP boundary with environment-safe base URLs and correct CORS behavior.

Concrete deliverable

A typed API client boundary that can call the NestJS health/tasks endpoints from the appropriate Next.js execution context.

Figure 6.1 — Where this phase lives in TaskFlow
Next.js component/action
API client
HTTP request
CORS/origin rules
NestJS
HTTP response
UI state
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

Keep the browser dependent on a stable API contract rather than importing backend implementation details.

02

Configure CORS narrowly for the origins that should call the API.

03

Centralize base URLs and environment-specific configuration instead of hard-coding localhost throughout components.

04

Trace failures across browser network tools, API logs, and server responses rather than guessing from UI symptoms.

Code-along path

Guided phase lab

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

  1. 01

    Define API base URLs per environment instead of hard-coding localhost throughout components.

  2. 02

    Call a public endpoint from a Server Component and inspect where the request originates.

  3. 03

    Call an endpoint from a Client Component and observe browser CORS behavior.

  4. 04

    Configure NestJS CORS for the exact development origin and credentials policy required.

  5. 05

    Centralize response parsing and non-2xx error handling in one API helper.

  6. 06

    Use Network tab plus API logs to trace one failing request end-to-end.

TerminalVerification commands
pnpm --filter web dev
pnpm --filter api start:dev

Predict what each command proves before running it. A command is useful only when its output answers a question.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Using the browser-only URL from server-to-server code in deployment

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

Failure drill 2

Wildcard CORS with credentials

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

Failure drill 3

Ignoring non-2xx fetch responses because fetch did not throw

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

Failure drill 4

Duplicating fetch/error logic in every component

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 Two Apps to One System

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 From Two Apps to One System VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS From Two Apps to One System Execution environments, network paths and the

3 min
02

API URLs and Environment Boundaries

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 API URLs and Environment Boundaries VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS API URLs and Environment Boundaries Configuration, NEXT_PUBLIC_ a

2 min
03

Server Side Fetch with Next.js

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 Server-Side Fetch with Next.js VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS Server-Side Fetch with Next.js Server Component -> NestJS before brows

3 min
04

Browser Fetch and CORS

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 Browser Fetch and CORS VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS Browser Fetch and CORS Client Component -> NestJS and the browser security bou

3 min
05

CORS Preflight and Fetch Error Semantics

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 CORS Preflight and Fetch Error Semantics VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS CORS Preflight and Fetch Error Semantics OPTIONS, JSON POST,

3 min
06

Clean API Client Boundary

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 A Clean API Client Boundary VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS A Clean API Client Boundary Centralize transport details without prematur

2 min
07

Loading Error Empty and Success States

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 Loading, Error, Empty and Success States VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS Loading, Error, Empty and Success States Make network depend

2 min
08

DevTools and End to End Debugging

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 DevTools and End-to-End Debugging VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS DevTools and End-to-End Debugging Trace browser, Next.js and NestJS

3 min
09

Hands On Lab and Phase Exit

FULL_STACK_DEVELOPER_2026_COMPLETE - Phase 06 Hands-On Lab and Phase Exit VOLUME 02 - FRONTEND + BACKEND FOUNDATIONS Hands-On Lab and Phase Exit Build, break, debug and explain the first com

3 min
LAB

Guided Build Lab

Purpose Connect Next.js and NestJS through an explicit, debuggable HTTP boundary with environment-safe base URLs and correct CORS behavior.

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 06 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 06 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Define API base URLs per environment instead of hard-coding localhost throughout components.

Practice

Call an endpoint from a Client Component and observe browser CORS behavior.

Debug

reproduce and diagnose — Using the browser-only URL from server-to-server code in deployment.

Independent

deliver A typed API client boundary that can call the NestJS health/tasks endpoints from the appropriate Next.js execution context.

Explain-back

Can locate a failure on frontend, network, or API side and Can explain CORS as a browser policy.

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 06's engineering goal?
02Which practice best matches Phase 06's engineering goal?
03Which practice best matches Phase 06's engineering goal?
04Which practice best matches Phase 06's engineering goal?
05You are working through the Phase 06 lab. Which next action is the most disciplined step?
06You are working through the Phase 06 lab. Which next action is the most disciplined step?
07You are working through the Phase 06 lab. Which next action is the most disciplined step?
08You are working through the Phase 06 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_End_to_End_Snapshot/apps/api/package.json
{
  "name": "api",
  "private": true,
  "scripts": {
    "build": "nest build",
    "start": "nest start",
    "start:dev": "nest start --watch"
  },
  "dependencies": {
    "@nestjs/common": "12.0.3",
    "@nestjs/core": "12.0.3",
    "@nestjs/platform-fastify": "12.0.3",
    "reflect-metadata": "^0.2.2",
    "rxjs": "^7.8.2"
  },
  "devDependencies": {
    "@nestjs/cli": "12.0.3",
    "@nestjs/schematics": "12.0.3",
    "@nestjs/testing": "12.0.3",
    "typescript": "~6.0.2"
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/app.module.ts
import { Module } from '@nestjs/common';
import { HealthModule } from './health/health.module.js';
import { TodosModule } from './todos/todos.module.js';

@Module({
  imports: [HealthModule, TodosModule],
})
export class AppModule {}
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthService } from './health.service.js';

@Controller('health')
export class HealthController {
  constructor(private readonly healthService: HealthService) {}

  @Get()
  getHealth() {
    return this.healthService.getStatus();
  }
}
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/health/health.module.ts
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller.js';
import { HealthService } from './health.service.js';

@Module({
  controllers: [HealthController],
  providers: [HealthService],
})
export class HealthModule {}
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/health/health.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class HealthService {
  getStatus() {
    return {
      status: 'ok',
      service: 'taskflow-api',
    };
  }
}
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/main.ts
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  app.enableCors({
    origin: [
      'http://localhost:3000',
      'http://127.0.0.1:3000',
    ],
  });

  app.enableShutdownHooks();

  const port = Number(process.env.PORT ?? 4000);
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/todos/todos.controller.ts
import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { TodosService } from './todos.service.js';

@Controller('todos')
export class TodosController {
  constructor(private readonly todosService: TodosService) {}

  @Get()
  findAll() {
    return this.todosService.findAll();
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/todos/todos.module.ts
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller.js';
import { TodosService } from './todos.service.js';

@Module({
  controllers: [TodosController],
  providers: [TodosService],
})
export class TodosModule {}
reference/TaskFlow_End_to_End_Snapshot/apps/api/src/todos/todos.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable()
export class TodosService {
  private nextId = 3;
  private readonly todos: Todo[] = [
    { id: 1, title: 'Learn HTTP', completed: false },
    { id: 2, title: 'Build the API contract', completed: false },
  ];

  findAll() {
    return this.todos;
  }
reference/TaskFlow_End_to_End_Snapshot/apps/web/.env.example
API_BASE_URL=
NEXT_PUBLIC_API_BASE_URL=
reference/TaskFlow_End_to_End_Snapshot/apps/web/package.json
{
  "name": "web",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "next": "16.3.3",
    "react": "19.3.0",
    "react-dom": "19.3.0"
  }
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/globals.css
:root {
  --background: #f6f8fa;
  --surface: #ffffff;
  --text: #17212b;
  --border: #d8e0e6;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--background);
  color: var(--text);
  font-family: Arial, sans-serif;
}

main {
  width: min(1100px, calc(100% - 2rem));
  margin: 0 auto;
  padding: 2rem 0;
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/layout.tsx
import type { Metadata } from "next";
import Link from "next/link";
import "./globals.css";

export const metadata: Metadata = {
  title: "TaskFlow",
  description: "Project and task management for focused teams",
};

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>
        <header>
          <Link href="/">TaskFlow</Link>
          <nav aria-label="Primary navigation">
            <Link href="/projects">Projects</Link>
            <Link href="/settings">Settings</Link>
          </nav>
        </header>
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/page.tsx
export default function HomePage() {
  return (
    <section>
      <p>TaskFlow</p>
      <h1>Keep work visible and moving.</h1>
      <p>Projects, tasks, ownership, and progress will grow here throughout the course.</p>
    </section>
  );
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/projects/page.tsx
const projects = [
  { id: 1, name: "Website refresh", status: "Active" },
  { id: 2, name: "Inventory rollout", status: "Planning" },
];

export default function ProjectsPage() {
  return (
    <section>
      <h1>Projects</h1>
      {projects.map((project) => (
        <article key={project.id}>
          <h2>{project.name}</h2>
          <p>{project.status}</p>
        </article>
      ))}
    </section>
  );
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/settings/density-toggle.tsx
"use client";

import { useState } from "react";

export function DensityToggle() {
  const [compact, setCompact] = useState(false);
  return (
    <button onClick={() => setCompact((value) => !value)}>
      Density: {compact ? "Compact" : "Comfortable"}
    </button>
  );
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/settings/page.tsx
import { DensityToggle } from "./density-toggle";

export default function SettingsPage() {
  return (
    <section>
      <h1>Settings</h1>
      <DensityToggle />
    </section>
  );
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/todos/page.tsx
import { fetchTodos } from '@/lib/api/todos';

export default async function TodosPage() {
  const apiBaseUrl = process.env.API_BASE_URL;

  if (!apiBaseUrl) {
    throw new Error('API_BASE_URL is not configured');
  }

  const todos = await fetchTodos(apiBaseUrl);

  return (
    <main>
      <h1>Todos from NestJS</h1>
      {todos.length === 0 ? (
        <p>No Todos yet.</p>
      ) : (
        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/todos-browser/browser-todos.tsx
'use client';

import { useCallback, useEffect, useState } from 'react';
import { createTodo, fetchTodos, type Todo } from '@/lib/api/todos';

export function BrowserTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;

  const loadTodos = useCallback(async () => {
    if (!baseUrl) {
      setError('NEXT_PUBLIC_API_BASE_URL is not configured');
      setLoading(false);
      return;
    }

    setLoading(true);
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/app/todos-browser/page.tsx
import { BrowserTodos } from './browser-todos';

export default function TodosBrowserPage() {
  return (
    <main>
      <h1>Browser-to-API Todos</h1>
      <BrowserTodos />
    </main>
  );
}
reference/TaskFlow_End_to_End_Snapshot/apps/web/src/lib/api/todos.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function expectJson<T>(response: Response): Promise<T> {
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} for ${response.url}`);
  }

  return response.json() as Promise<T>;
}

export async function fetchTodos(baseUrl: string): Promise<Todo[]> {
  const response = await fetch(`${baseUrl}/todos`, { cache: 'no-store' });
  return expectJson<Todo[]>(response);
}

export async function createTodo(baseUrl: string, title: string): Promise<Todo> {