Our project is a Job Board and Career Portal that allows employers to post available jobs and for applicants to search and apply for jobs/positions. The platform supports multiple user roles including unregistered visitors, registered users (both employers and applicants), and administrators. Unregistered users can browse and search job postings without creating an account. Registered users must create a profile by signing up, and they must declare themselves to be either an employer or an applicant. Employers can post, edit, and manage their own job listings. Applicants can apply to jobs, upload their resume, and manage their personal profiles. Administrators oversee the platform by moderating content, managing user accounts, and viewing data analytics (e.g., user activity, number of job postings, number of users, number of employers). The goal of the system is to provide a structured, searchable job platform that demonstrates full-stack development using the MERN stack including authentication, role-based permissions, database-driven content, and responsive frontend design.
We use a 3 layered architecture to keep our software and data secure while improving performance and eliminating the possibility of bugs.
- Controller: Handles the HTTP routing, data validation and constucting response objects. This is where we define routes (GET or POST). It reads from the HTTP request sent by the frontend, calls the service and returns the results as a JSON response to the frontend.
- Service: Handles the business logic. No HTTP or direct databse access. Once the controller calls a service method the service then calls the method in the repository that gets the relevant data. Once the data or a response is returned to the service it will be transferred to the controller to be returned to the frontend. This layer is also be responsible for operations of data such as hashing passwords when doing authentication.
- Repository: Handles the database operations, models and mock data.
Flow Example (get one applicant): The controller receives GET /api/applicants/123 from the frontend, reads req.params.id and
calls applicantService.getApplicantById('123'). Service receives the request by the controller and passes the id into
applicantRepository.findById(id). The repository receives this request and runs the MongoDB query which returns the document to the service. The service sends the document to the controller which will transform it to JSON and sends it to frontend.
-
Add you env
Create a .env file inside
/server/and paste the reguired text shared with you.
-
Start Docker
Ensure Docker is installed and running on your machine before continuing.
-
Build containers
From the root directory
/, run:docker-compose up --build
-
Subsequent Runs/Regular Docker Startup
After initial build, you can start app with
docker-compose up
-
Stopping the Applicantion
docker-compose down
-
Start Up Containers
In a terminal, activate the containers from
/, as we will need server running to execute seed.docker-compose up
-
Seed Database
In a seperate terminal, from
/as well, execute this commanddocker-compose exec server node scripts/seed.js
Frontend tests (from the client directory):
cd client
npm testTests use Jest with React Testing Library. The project is configured with:
jest.config.cjs(Jest config)babel.config.cjs(Babel config for JSX transformation)src/setupTests.js(Test environment setup)
Base URL for API: /api.
| Method | Endpoint | Description | Response |
|---|---|---|---|
| GET | /api/health |
Health check | { status, timestamp } |
| Method | Endpoint | Description | Request | Response |
|---|---|---|---|---|
| POST | /api/auth/register |
Register a new user | Body: role, email, password, name |
{ user, token } |
| POST | /api/auth/login |
Log in and get a user + JWT | Body: email, password, role |
{ user, token } |
| PUT | /api/auth/changepassword |
Change user's password | Header: Authorization: Bearer <token>. Body: currentPassword, newPassword |
{ _id, email, name?, role, ... } |
| PUT | /api/auth/changeemail |
Change user's email | Header: Authorization: Bearer <token>. Body: newEmail, password |
{ _id, email, name?, role, ... } |
| Method | Endpoint | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/applicants |
List all applicants | — | Array of applicants (no password, pfp, or resume) |
| GET | /api/applicants/:id |
Get one applicant by ID | — | Single applicant (no password, pfp, or resume) |
| GET | /api/applicants/:id/pfp |
Get applicant's profile picture (or default) | — | Image body; Content-Type set |
| GET | /api/applicants/:id/resume |
Get applicant's resume (download or view) | Optional: ?inline=1 to view in browser |
PDF body; Content-Disposition attachment or inline |
| POST | /api/applicants/:id/delete |
Delete an applicant account | — | { deleted: true } |
| PUT | /api/applicants/:id/pfp |
Upload or replace applicant's profile picture | multipart/form-data with file |
Updated applicant object |
| POST | /api/applicants/:id/resume |
Upload or replace applicant's resume | multipart/form-data with file |
Updated applicant object |
| Method | Endpoint | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/companies |
List all companies | — | Array of companies (no password or pfp) |
| GET | /api/companies/:id |
Get one company by id | — | Single company (no password or pfp) |
| GET | /api/companies/:id/job-postings |
Job postings for company | — | Array of job postings |
| GET | /api/companies/:id/analytics |
Company hiring analytics | — | { totalJobs, closedJobs, avgPostingDurationDays, fillRate } |
| GET | /api/companies/:id/pfp |
Get company's profile picture (or default) | — | Image body; Content-Type set |
| PUT | /api/companies/:id/pfp |
Upload or replace company's profile picture | multipart/form-data with file |
Updated company object |
| POST | /api/companies/:id/delete |
Delete a company account | — | { deleted: true } |
| POST | /api/companies/:id/create-job |
Create a job posting for this company | Body: title, optional location, description, tags |
Created job object |
| Method | Endpoint | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/job-postings |
List all job postings | Query: optional status, companyId, limit, skip |
Array of job postings |
| GET | /api/job-postings/:id |
Get one job posting | — | Single job posting |
| POST | /api/job-postings/:id/apply |
Apply to job (appliscant only) | Header: Authorization: Bearer <token>. Applicant must have a resume on their profile. Job must be ACTIVE. |
{ applied: true, job } |
| PUT | /api/job-postings/:id |
Update a job posting | Header: Authorization: Bearer <token> (company owner or admin). Body: title, tags, location, description, status (all optional) |
Updated job object |
| PATCH | /api/job-postings/:id/status |
Change job status | Header: Authorization: Bearer <token> (company owner or admin). Body: { "status": "ACTIVE" | "UNPUBLISHED" | "CLOSED" } |
Updated job object |
| DELETE | /api/job-postings/:id |
Delete a job posting | Header: Authorization: Bearer <token> (company owner or admin) |
{ deleted: true } |
| GET | /api/job-postings/:id/applications |
Get recent applications for a job | Header: Authorization: Bearer <token> (company owner). Query: optional limit (default 50, max 100) |
Array of applicants |
| Method | Endpoint | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/admin/:id/pfp |
Get administrator's profile picture (or default) | — | Image body; Content-Type set |
| PUT | /api/admin/:id/pfp |
Upload or replace administrator's profile picture | multipart/form-data with file |
Updated administrator object |
- JWT: Login and register returns a
token, the client sends it asAuthorization: Bearer <token>on protected requests. - Protected routes require a valid JWT. The user’s id and role are taken from the token, not the request body for security.
- Change password/email require the current password and a valid JWT.
Uploading a file: When a file is uploaded on the frontend it is sent to the backend as a multipart/form-data.
On the backend it gets parsed by Multer to be held by in memory as a Buffer. The controller passes that Buffer down to service, then to the repository where it gets stored in the DB.
Downloading a file: The repository loads the file from the DB. The service returns the file and content type. The controller sends the buffer to the browser.
When you add a new feature, say "job postings":
server/repository/models/jobPosting.model.js- Mongoose schemaserver/repository/jobPosting.repository.js- Database operationsserver/service/jobPosting.service.js- Business logicserver/controller/jobPosting.controller.js- Routes & handlers- Register routes in
server/server.js
