Back
Interactive Explainer

Build Challenge: Full-Stack Job Board

Week 1 at a seed-stage startup. The CEO needs a job board live by Friday. You have a Next.js template and a Postgres database. Ship three pages, pass a load test, and deploy. Your first code review is Monday.

Relevant for:JuniorMid-level
Why this matters at your level
Junior

Complete the full build: all 3 routes, NextAuth session, Prisma schema, deployed to Vercel. Able to explain every line of code in the build.

Mid-level

Add: cursor pagination, full-text search, rate limiting on form submit, proper 404/error pages, .env.example, load test with k6. Able to explain the 10x scaling plan.

Build Challenge: Full-Stack Job Board

Week 1 at a seed-stage startup. The CEO needs a job board live by Friday. You have a Next.js template and a Postgres database. Ship three pages, pass a load test, and deploy. Your first code review is Monday.

~3 min read
Be the first to complete!
LEVEL 1 BUILD CHALLENGE

The most common full-stack take-home test: build a job board. Sounds simple. The trap is the details. Every recruiter who gives this test has seen a specific class of mistakes: missing auth, SQL injection in the search field, N+1 queries on the job list, no error handling, and a deployment that only works on localhost. This challenge is that take-home — made real.

The question this raises

Can you wire up a full Next.js + Postgres + Auth stack from scratch without a tutorial?

Test your assumption first

A job board search endpoint builds a SQL query by concatenating user input: SELECT * FROM jobs WHERE title LIKE '%' + searchQuery + '%'. A user searches for: '' OR 1=1; DROP TABLE jobs; --. What happens?

Lesson outline

Before the build: two desks, one project

How this concept changes your thinking

Situation
Before
After

The job listing page loads

SELECT * FROM jobs — returns all 10,000 jobs. Frontend renders them all. Page is 4MB. Browser OOM.

Prisma findMany with take: 20, cursor-based pagination. Frontend renders 20 at a time. Page is 8KB.

The admin route needs protection

Admin page has no auth check. Anyone who guesses /admin/jobs/new can post jobs. No one noticed.

NextAuth + middleware.ts: redirect unauthenticated users to /login. Server action checks session before any DB write.

The search field

Raw SQL string concatenation with user input — SQL injection. A user enters: DROP TABLE jobs. The table is gone.

Prisma parameterized queries: db.job.findMany({ where: { title: { contains: query } } }) — injection is impossible.

The SRE take-home checklist

The 5 bugs every recruiter tests for in a job board take-home

1. N+1 query: SELECT jobs returns 20 rows, then SELECT company WHERE id=? fires 20 times. Use Prisma include. 2. SQL injection in the search box. Use parameterized queries. 3. No auth on admin routes. Use NextAuth middleware. 4. No error page — server crash shows raw stack trace to users. Add error.tsx. 5. Works locally, fails on Vercel — missing environment variable. Use .env.example.

What the code review looks for

Senior engineers reviewing this take-home check two things first: does the schema have the right indexes, and does the admin route check session before touching the database.

job-board-schema.prisma
1// prisma/schema.prisma
2model Job {
3 id String @id @default(cuid())
4 title String
5 company String
6 location String
7 description String @db.Text
8 salary Int? // cents, never floats for money
9 createdAt DateTime @default(now())
10 updatedAt DateTime @updatedAt
11
12 // Index for the listing page sort (most recent first)
13 @@index([createdAt(sort: Desc)])
14}
15
16model User {
17 id String @id @default(cuid())
18 email String @unique
19 role String @default("user") // "user" | "admin"
20 accounts Account[]
21 sessions Session[]
22}
job-listing-page.tsx
1// app/jobs/page.tsx — Server Component (no loading state, data in HTML)
2import { db } from '@/lib/db';
3
4export default async function JobsPage({
5 searchParams,
6}: {
7 searchParams: { cursor?: string }
8}) {
9 const jobs = await db.job.findMany({
10 take: 20,
11 ...(searchParams.cursor && {
12 skip: 1,
13 cursor: { id: searchParams.cursor },
14 }),
15 orderBy: { createdAt: 'desc' },
16 // include company data in one query — NOT a separate query per job
17 });
18
19 const nextCursor = jobs.length === 20 ? jobs[jobs.length - 1].id : undefined;
20
21 return (
22 <div>
23 {jobs.map(job => <JobCard key={job.id} job={job} />)}
24 {nextCursor && (
25 <a href={`/jobs?cursor=${nextCursor}`}>Load more</a>
26 )}
27 </div>
28 );
29}
30
31// app/admin/jobs/new/page.tsx — protected route
32import { getServerSession } from 'next-auth';
33import { redirect } from 'next/navigation';
34
35export default async function NewJobPage() {
36 const session = await getServerSession();
37 if (!session || session.user.role !== 'admin') {
38 redirect('/login'); // server-side redirect before any HTML is sent
39 }
40 return <NewJobForm />;
41}

The four pillars of the build

What to build and why each piece matters

  • /jobs — paginated job listing (Server Component)Cursor-based pagination (not page=1). Server Component so data is in the HTML — no loading spinner for initial render. Tests: does the page work with 0 jobs? 1 job? 10,000 jobs?
  • /jobs/[id] — job detail (Server Component + generateStaticParams)Static generation for popular job IDs, dynamic for new ones. notFound() from next/navigation when job does not exist — returns 404 not a crash.
  • /admin/jobs/new — protected form (Server Action)NextAuth session check at top of Server Action. Zod validation before DB write. revalidatePath after creation. Redirect to the new job page.
  • Error boundaries and not-found pagesapp/error.tsx — catches runtime errors, shows "Something went wrong" not a stack trace. app/not-found.tsx — 404 page. Both required for production.

How companies ship this stack

The Server Action pattern: auth check first, validate input with Zod, write to DB via Prisma (parameterized, injection-proof), revalidate cache, redirect.

The interview question they always ask: "What would you do differently at 10x scale?"

Prepared answer: add a full-text search index (Postgres tsvector or Elasticsearch), add a Redis cache for the job listing (TTL 60s), add a background queue (BullMQ) for email notifications on application submit, add cursor-based infinite scroll instead of page links, split read replicas for the listing page. This answer shows you understand the present system AND the scaling path.

server-action-create-job.ts
1// app/admin/jobs/new/actions.ts
2'use server';
3import { z } from 'zod';
4import { getServerSession } from 'next-auth';
5import { revalidatePath } from 'next/cache';
6import { redirect } from 'next/navigation';
7import { db } from '@/lib/db';
8
9const CreateJobSchema = z.object({
10 title: z.string().min(3).max(100),
11 company: z.string().min(2).max(100),
12 location: z.string().min(2),
13 description: z.string().min(50),
14 salary: z.coerce.number().positive().optional(),
15});
16
17export async function createJob(formData: FormData) {
18 // 1. Auth check first — fail fast
19 const session = await getServerSession();
20 if (!session || session.user.role !== 'admin') {
21 throw new Error('Unauthorized');
22 }
23
24 // 2. Validate — Prisma injection is safe, but we still need shape validation
25 const result = CreateJobSchema.safeParse(Object.fromEntries(formData));
26 if (!result.success) {
27 return { error: result.error.flatten() };
28 }
29
30 // 3. Write — parameterized by Prisma, no SQL injection possible
31 const job = await db.job.create({ data: result.data });
32
33 // 4. Invalidate cache for the listing page
34 revalidatePath('/jobs');
35
36 // 5. Redirect to the new job
37 redirect(`/jobs/${job.id}`);
38}

Exam Answer vs. Production Reality

1 / 2

Server Actions vs API Routes

📖 What the exam expects

Server Actions are async functions that run on the server, invoked directly from Client Components. API Routes are HTTP endpoints at /api/*.

Toggle between what certifications teach and what production actually requires

How this might come up in interviews

Build challenges test whether you can ship a complete, production-ready feature — not just a working prototype.

Common questions:

  • Walk me through your Prisma schema choices — why those indexes?
  • How does your admin route stay secure if someone bypasses the middleware?
  • What happens if a user submits the job form twice (double submit)?
  • How would you add full-text search without Elasticsearch?
  • Deploy this to Vercel with a Neon Postgres database — what env vars do you need?

Strong answers include:

  • Has .env.example committed alongside the code
  • Validates input with Zod before touching the database
  • Uses cursor-based pagination, not offset pagination
  • Can explain the 10x scaling answer without being prompted

Red flags:

  • Admin route has no server-side session check
  • Search field uses string concatenation in SQL
  • No error.tsx or not-found.tsx
  • Cannot explain what revalidatePath does

Ready to see how this works in the cloud?

Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.

View role-based paths

Sign in to track your progress and mark lessons complete.

Discussion

Questions? Discuss in the community or start a thread below.

Join Discord

In-app Q&A

Sign in to start or join a thread.