Skip to content
    SeaCode
    Learning tracksCode ArenaWorkshops & EventsInstructors
    Log inSign up
    NNext.js

    Next.js foundations

    • What is Next.js?
    • The App Router: folders are routes
    • Dynamic routes
    • Client-side navigation

    Server, client and data

    • Server and Client Components
    • Fetching data on the server
    • loading, error and not-found
    • Caching and revalidation

    Mutations, APIs and security

    • Server Actions
    • Route Handlers: building an API
    • Proxy (formerly Middleware)
    • Authentication and protecting pages

    Going to production

    • Metadata and SEO
    • Images, fonts and performance
    • Environment variables and deploying to Vercel
    • Project: a course catalogue
    Next.js

    Going to production · Lesson 16 of 16

    Project: a course catalogue

    A server page, client search, an API and a server action, together.

    15 minutes3 games

    We'll design a course catalogue like SeaCode's, tying together everything we've learned. Here's the file layout; then you'll build the parts that can run here: the client search, the API, and the action's validation.

    • app/courses/
      • page.tsx (server): reads courses with "use cache"
      • loading.tsx: skeleton cards
      • course-search.tsx ("use client"): filters on the client
      • [slug]/page.tsx: generateMetadata + notFound()
      • actions.ts ("use server"): enroll(), updateTag("courses")
      • ../api/courses/route.ts: GET JSON for the mobile app
    TypeScript
    // app/courses/page.tsx: the server part
    import { getCatalog } from "@/lib/courses"      // "use cache" + cacheTag("courses")
    import { CourseSearch } from "./course-search"
    
    export const metadata = { title: "Courses" }
    
    export default async function CoursesPage() {
      const courses = await getCatalog()
      // Only plain data crosses into the client component
      return <CourseSearch courses={courses.map(({ slug, title, track, price }) => ({ slug, title, track, price }))} />
    }

    1. Search (a client component)

    Exercise

    CourseSearch receives courses from the server. It has a search input (case-insensitive), a select for the track (all, web or data), and a first line reading N results. The checker types s into the search.

    The page should show:
    3 results
    all
    web
    data
    HTML & CSS
    JavaScript
    SQL
    React
    const COURSES = [
      { slug: "html-css", title: "HTML & CSS", track: "web", price: 0 },
      { slug: "javascript", title: "JavaScript", track: "web", price: 450 },
      { slug: "python", title: "Python", track: "data", price: 400 },
      { slug: "sql", title: "SQL", track: "data", price: 300 },
    ]
    
    function CourseSearch({ courses }) {
      return (
        <div>
          <p>{courses.length} results</p>
          <input placeholder="Search" />
          <ul>{courses.map((c) => <li key={c.slug}>{c.title}</li>)}</ul>
        </div>
      )
    }
    
    function App() {
      return <CourseSearch courses={COURSES} />
    }

    2. The API (a route handler)

    Exercise

    GET returns the courses as JSON. With ?track=, return only that track's courses. If the track isn't web or data, return { "error": "unknown track" } with 400. Print the status and course count (or the error) for the three requests.

    Expected output:
    200 3 courses
    200 1 courses
    400 unknown track
    JavaScript
    const COURSES = [
      { slug: "html-css", track: "web" },
      { slug: "javascript", track: "web" },
      { slug: "python", track: "data" },
    ]
    
    async function GET(request) {
      return Response.json(COURSES)
    }
    
    for (const q of ["", "?track=data", "?track=games"]) {
      const res = await GET(new Request("https://seacode.tech/api/courses" + q))
      const body = await res.json()
      console.log(res.status, Array.isArray(body) ? body.length + " courses" : body.error)
    }

    3. Enrolling (a server action)

    Exercise

    Write the logic of enroll(user, formData, enrolled): no user returns sign in first; a slug not in CATALOG returns no such course; already in enrolled returns already enrolled; otherwise enrolled in SLUG.

    Expected output:
    sign in first
    no such course
    already enrolled
    enrolled in javascript
    JavaScript
    const CATALOG = ["html-css", "javascript", "python"]
    
    function enroll(user, formData, enrolled) {
      return "todo"
    }
    
    const form = (slug) => { const f = new FormData(); f.set("slug", slug); return f }
    console.log(enroll(null, form("python"), []))
    console.log(enroll({ id: "u1" }, form("rust"), []))
    console.log(enroll({ id: "u1" }, form("python"), ["python"]))
    console.log(enroll({ id: "u1" }, form("javascript"), ["python"]))

    Important

    Notice the order of checks in the action: who (the user) → what (does the course exist?) → allowed? (not enrolled, seats, payment) → do it → updateTag. This order repeats in every real action and avoids doing server work for a request that will be rejected anyway.

    Interview question

    Walk through how you'd build a course catalogue page in Next.js.

    Learn more from

    • Next.js docs: Getting startednextjs.org
    • Next.js Learn: Dashboard app coursenextjs.org
    • Vercel templates: Next.jsvercel.com

    Played 0 of 3 games

    Sign in to save your progress and earn XP.

    PreviousEnvironment variables and deploying to Vercel🎉 خلّصت Next.js!
    SeaCode

    Learn to code for real, online or at our branch in front of Cairo University, Giza.

    Learn

    • All courses
    • Interactive tutorials
    • Interview practice problems
    • Mock interview
    • Learning tracks
    • Code Arena
    • Workshops & Events
    • Knowledge library
    • Team projects

    Your account

    • Log in
    • Sign up
    • Learning plan
    • Certificates
    • Site map

    Contact us

    • WhatsApp
    • 010 9094 1104
    • In front of Cairo University, Giza

    © 2026 SeaCode. All rights reserved.