Job Board Platform
Part of the Full Stack Developer Roadmap
In short
A job board has two kinds of users who want opposite things. Employers post and review, candidates search and apply. Getting roles and ownership right is the whole project.
A platform with two user types — employers posting jobs, candidates applying — practice for role-based features and more complex, multi-sided data relationships.

Think of it like this
The college placement cell
Companies drop job details at the placement cell. Students drop their applications. The coordinator makes sure each company sees only its own applicants, and no student applies to the same company twice.
Watch it happen
Two roles, one database
POST /jobs
1/6An employer posts a job. The API checks the role is EMPLOYER and saves employer_id with it.
The big ideas
What you need to know
Roles on every route
Each endpoint says who may call it. Check the role on the server, not just by hiding buttons.
Ownership beats role
Being an employer isn't enough. The job must belong to you before you see its applicants.
Search with filters
Location, remote, salary and skills filters with Postgres full-text search or a simple ILIKE to start.
Status updates
Applications move through applied, shortlisted, rejected. Email the candidate on each change from a background job.
See it in code
Walk through the code
1const requireRole = (role: "EMPLOYER" | "CANDIDATE") =>2 (req: Request, res: Response, next: NextFunction) =>3 req.user?.role === role ? next() : res.status(403).json({ error: { code: "FORBIDDEN" } });4 5app.post("/jobs/:id/apply", requireRole("CANDIDATE"), async (req, res) => {6 const { count } = await db.application.createMany({7 data: [{ jobId: req.params.id, userId: req.user.id }], skipDuplicates: true,8 });9 res.status(count ? 201 : 409).end();10});11 12app.get("/jobs/:id/applicants", requireRole("EMPLOYER"), async (req, res) => {13 const job = await db.job.findFirst({ where: { id: req.params.id, employerId: req.user.id } });14 if (!job) return res.status(404).end(); // not yours, or doesn't exist15 res.json({ data: await db.application.findMany({ where: { jobId: job.id }, include: { user: true } }) });16});1/3A small middleware factory: requireRole("EMPLOYER") blocks everyone else with 403.
A role middleware, an apply route that relies on a unique constraint, and an applicants route that checks ownership before returning anything.
Save yourself the pain
Common mistakes
Oops
Checking only that the caller is an employer before showing applicants.
Do this instead
Also check the job belongs to that employer. Otherwise any company can read every resume.
Oops
Making resume files public URLs anyone can guess.
Do this instead
Store them privately and hand out short-lived signed URLs only to the job's owner.
Your mission
First milestone: post and apply
Quick check
Employer B calls GET /jobs/12/applicants for a job owned by Employer A. What should happen?
Go deeper
- 1Model two distinct user roles with different dashboards and permissions.
- 2Add search and filtering for job listings.
- 3Build an application-tracking flow for both employers and candidates.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.