Stop 19 · Build Full-Stack Portfolio ProjectsProjects · 6/12

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.

The purple blob at a desk matches folders from briefcases on one side and backpacks on the other.

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

Employer
API
Database
Candidate

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

ts
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

~45 min

Quick check

Employer B calls GET /jobs/12/applicants for a job owned by Employer A. What should happen?

Go deeper

  1. 1Model two distinct user roles with different dashboards and permissions.
  2. 2Add search and filtering for job listings.
  3. 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.

Download
InsideEdge