Building a Job Board in an Afternoon with Next.js and CleanJobData
TL;DR
A Next.js job board built on CleanJobData needs three layers: an API client, server actions that keep the key server-side, and pages for listings, details, filters, and SEO. Cursor pagination (next_page/prev_page/cursor are interchangeable) drives the listings view, and each job detail page should carry JobPosting structured data — never the listings page itself. The starter template handles steps 1-4 in about ten minutes if you'd rather deploy than build each layer by hand.
Building a job board from scratch usually means building far more than a UI. You need job ingestion, search, filters, pagination, location normalization, salary parsing, SEO pages, and a way to keep listings fresh. CleanJobData removes most of that infrastructure by giving you a normalized jobs API that can power a Next.js job board directly.
This walks through building one yourself, layer by layer, so you know exactly what's happening at each step — API client, server-side data fetching, job cards, filters, detail pages. If you'd rather skip straight to a working deploy, the job board starter template does steps 1-4 below for you in about ten minutes: clone the repo, set two environment variables, deploy. Read this instead if you're integrating the API into an existing codebase, want to understand the pieces before you commit to the template, or are building something the template doesn't cover out of the box.
Every request below needs an API key: sign up for a free account, generate one from your dashboard, and set it as CLEANJOBDATA_API_KEY in your environment — never expose it in client-side code. The example requests use the real base URL, https://api.cleanjobdata.com.
What You Get
The API returns stable job objects instead of raw ATS-specific responses. A typical job includes:
titlelocation— a single display string (e.g."San Francisco, CA; Remote"), good for showing as-islocations— the same location, structured into an array of resolved city/state/country rows for filtering and map views; see Interpreting the Job Location Object for the full shapeapplication_urlpublishedhas_remoteemployment_typesalary_minsalary_maxsalary_currencysalary_textexperience_level— a single best-guess seniority level, for a simple filter or badgeexperience_levels— the full array a posting can match (a listing spanning Mid and Senior returns both), for anything that needs the complete setcompany- optional
description
That structure is enough to build cards, detail pages, filters, SEO landing pages, and analytics without writing custom parsers.
Starter Architecture
A simple Next.js job board can be built around three layers:
- API client — fetches jobs from CleanJobData with
Authorization: Bearer <key>. - Server actions — call the API from the server and pass typed data to the UI.
- Next.js pages — render listing pages, detail pages, feeds, and SEO content.
This keeps API keys server-side and lets the frontend work with clean JSON.
Basic Job Request
curl "https://api.cleanjobdata.com/jobs?title=frontend%20engineer&remote=true&experience_level=SE&location=US&limit=20" \
-H "Authorization: Bearer $CLEANJOBDATA_API_KEY"The response shape is:
{
"data": [],
"pagination": {
"limit": 20,
"next_page": "opaque-cursor",
"prev_page": null
},
"meta": {
"query_time_ms": 42,
"filters_applied": []
}
}Use next_page and prev_page for cursor pagination: pass whichever token you got back on your next request (as cursor, next_page, or prev_page — three interchangeable names for the same param) to move forward or backward through the result set. The direction comes from the token itself, not from which name you send it as.
Rendering a Job Card
type Job = {
id: number;
title: string;
location: string | null;
salary_min: number | null;
salary_max: number | null;
salary_currency: string | null;
experience_level: string | null;
company: {
name: string;
logo: string | null;
} | null;
};
function formatSalary(job: Job) {
if (!job.salary_min && !job.salary_max) return null;
const currency = job.salary_currency ?? "USD";
const min = job.salary_min ? Intl.NumberFormat("en-US", { style: "currency", currency }).format(job.salary_min) : null;
const max = job.salary_max ? Intl.NumberFormat("en-US", { style: "currency", currency }).format(job.salary_max) : null;
return [min, max].filter(Boolean).join(" - ");
}
export function JobCard({ job }: { job: Job }) {
return (
<article>
<h2>{job.title}</h2>
<p>{job.company?.name}</p>
<p>{job.location}</p>
{formatSalary(job) ? <p>{formatSalary(job)}</p> : null}
{job.experience_level ? <p>{job.experience_level}</p> : null}
</article>
);
}Keep the presentation flexible because salary and company data can be missing depending on the source.
Adding Filters
The same API powers your search and filter UI — see choosing filters for your use case for which ones to expose first:
curl "https://api.cleanjobdata.com/jobs?title=engineer&city_id=123&remote=true&experience_level=MI,SE&salary=100000,180000&sort_by=relevance" \
-H "Authorization: Bearer $CLEANJOBDATA_API_KEY"Useful query parameters:
titleorsearchcity_id,state_id,country_id, orlocationremote=trueexperience_level=EN,MI,SE,EXsalary=min,maxpublished_after=2026-06-01max_age=7dsort_by=publishedorsort_by=relevance
The backend validates these filters and returns the applied filters in meta.filters_applied.
Detail Pages
Request the description only when you need it:
curl "https://api.cleanjobdata.com/jobs/123?fields=title,location,description,company,application_url,published,salary_min,salary_max" \
-H "Authorization: Bearer $CLEANJOBDATA_API_KEY"The detail route should link to application_url for the actual employer application flow.
SEO and Feeds
Each job detail page should carry JobPosting structured data — it's what makes a listing eligible for Google's job-search rich result. Put it only on the detail route, never on a list/filtered page showing multiple jobs; that's a documented Google policy violation covered in the same guide. Once listings are live, submitting new ones to the Google Indexing API gets them crawled same-day instead of waiting on Google's normal sitemap cadence.
Normalized data makes SEO pages easier. You can generate pages for:
- remote jobs
- senior engineer jobs
- city-specific feeds
- state-specific feeds
- salary-filtered feeds
- company-domain feeds
Each page can use the same API filters and render a unique intro, problem statement, and FAQ from your content catalog. If you're adding this to an existing app rather than starting from the starter template, adding job search to an existing app covers the same integration from that angle.
Production Checklist
Before launching, verify:
- API key is stored server-side, never in client bundles.
- Pagination uses
next_pageandprev_page. - Missing salary, company, and location values are handled gracefully.
- Expired jobs are excluded by default.
- Location filters use
city_id,state_id,country_id, orlocationcountry codes. - Detail pages request
descriptiononly when needed. - SEO pages use stable slugs and canonical job data.
The Fast Path
CleanJobData gives you the data layer for a modern job board: normalized jobs, reliable filters, cursor pagination, and optional descriptions. Pair that with a Next.js App Router frontend and you can ship a useful job board without building scrapers, parsers, or a search infrastructure from scratch.