Going to production · Lesson 16 of 16
Project: a course catalogue
A server page, client search, an API and a server action, together.
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
// 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.
3 results all web data HTML & CSS JavaScript SQL
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.
200 3 courses 200 1 courses 400 unknown track
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.
sign in first no such course already enrolled enrolled in 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"]))Learn more from
Played 0 of 3 games
Sign in to save your progress and earn XP.