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.
Complete the full build: all 3 routes, NextAuth session, Prisma schema, deployed to Vercel. Able to explain every line of code in the build.
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.
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.
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?
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
How this concept changes your thinking
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 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.
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.
1// prisma/schema.prisma2model Job {3id String @id @default(cuid())4title String5company String6location String7description String @db.Text8salary Int? // cents, never floats for money9createdAt DateTime @default(now())10updatedAt DateTime @updatedAt1112// Index for the listing page sort (most recent first)13@@index([createdAt(sort: Desc)])14}1516model User {17id String @id @default(cuid())18email String @unique19role String @default("user") // "user" | "admin"20accounts Account[]21sessions Session[]22}
1// app/jobs/page.tsx — Server Component (no loading state, data in HTML)2import { db } from '@/lib/db';34export default async function JobsPage({5searchParams,6}: {7searchParams: { cursor?: string }8}) {9const jobs = await db.job.findMany({10take: 20,11...(searchParams.cursor && {12skip: 1,13cursor: { id: searchParams.cursor },14}),15orderBy: { createdAt: 'desc' },16// include company data in one query — NOT a separate query per job17});1819const nextCursor = jobs.length === 20 ? jobs[jobs.length - 1].id : undefined;2021return (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}3031// app/admin/jobs/new/page.tsx — protected route32import { getServerSession } from 'next-auth';33import { redirect } from 'next/navigation';3435export default async function NewJobPage() {36const session = await getServerSession();37if (!session || session.user.role !== 'admin') {38redirect('/login'); // server-side redirect before any HTML is sent39}40return <NewJobForm />;41}
What to build and why each piece matters
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.
1// app/admin/jobs/new/actions.ts2'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';89const CreateJobSchema = z.object({10title: z.string().min(3).max(100),11company: z.string().min(2).max(100),12location: z.string().min(2),13description: z.string().min(50),14salary: z.coerce.number().positive().optional(),15});1617export async function createJob(formData: FormData) {18// 1. Auth check first — fail fast19const session = await getServerSession();20if (!session || session.user.role !== 'admin') {21throw new Error('Unauthorized');22}2324// 2. Validate — Prisma injection is safe, but we still need shape validation25const result = CreateJobSchema.safeParse(Object.fromEntries(formData));26if (!result.success) {27return { error: result.error.flatten() };28}2930// 3. Write — parameterized by Prisma, no SQL injection possible31const job = await db.job.create({ data: result.data });3233// 4. Invalidate cache for the listing page34revalidatePath('/jobs');3536// 5. Redirect to the new job37redirect(`/jobs/${job.id}`);38}
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
Build challenges test whether you can ship a complete, production-ready feature — not just a working prototype.
Common questions:
Strong answers include:
Red flags:
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 pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.