Learn how to build a fully functional job board using Replit. Follow our step-by-step guide to create, customize, and launch your own job platform.

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
To build a job board on Replit, you’ll use a simple Node.js + Express backend with a lightweight frontend (HTML + CSS + JS). You’ll store job listings in memory first (for testing), then connect to Replit’s built-in SQLite database for persistence. Replit will host both the server and frontend together so you can run and preview it instantly. The backend handles job creation and listing retrieval through API routes, while the frontend displays jobs and allows creating new ones via form submission. This approach works well in Replit’s always-on environment and can later scale with external databases or Replit Deployments.
From Replit’s dashboard, create a new Repl using the “Node.js” template. This will automatically generate an index.js file (the entry file) and a package.json file (where dependencies go).
In the Replit shell pane at the bottom, run this command:
npm install express better-sqlite3
This sets up Express for your backend server and Better-SQLite3 for local database management (works smoothly on Replit without extra setup).
Replace the content of index.js with this real working code:
// Load required packages
const express = require('express');
const Database = require('better-sqlite3');
const app = express();
const db = new Database('database.db');
// Middleware to handle JSON bodies
app.use(express.json());
// Serve static frontend files from the 'public' folder
app.use(express.static('public'));
// Create a jobs table if it doesn't exist
db.prepare(`
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
company TEXT,
description TEXT
)
`).run();
// Route to get all jobs
app.get('/api/jobs', (req, res) => {
const jobs = db.prepare('SELECT * FROM jobs').all();
res.json(jobs);
});
// Route to add a new job
app.post('/api/jobs', (req, res) => {
const { title, company, description } = req.body;
if (!title || !company) {
return res.status(400).json({ error: 'Title and company are required' });
}
const stmt = db.prepare('INSERT INTO jobs (title, company, description) VALUES (?, ?, ?)');
const result = stmt.run(title, company, description);
res.json({ id: result.lastInsertRowid });
});
// Start server on Replit’s dynamic port
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Server running on port ${port}`));
This sets up your backend API. When you click “Run” in Replit, you should see “Server running on port…” in the console. You can now open the browser view (the web icon) and it will load your frontend from public/index.html.
Edit public/index.html with this minimal form and display section:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Replit Job Board</title>
<style>
body { font-family: Arial; padding: 20px; }
form { margin-bottom: 20px; }
input, textarea { display: block; margin: 5px 0; width: 300px; }
.job { border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; }
</style>
</head>
<body>
<h3>Post a Job</h3>
<form id="jobForm">
<input type="text" id="title" placeholder="Job Title" required />
<input type="text" id="company" placeholder="Company Name" required />
<textarea id="description" placeholder="Description"></textarea>
<button type="submit">Add Job</button>
</form>
<h3>Available Jobs</h3>
<div id="jobs"></div>
<script src="script.js"></script>
</body>
</html>
Edit public/script.js to handle form submission and fetch job data:
const form = document.getElementById('jobForm');
const jobsDiv = document.getElementById('jobs');
// Load jobs when page opens
async function loadJobs() {
const res = await fetch('/api/jobs');
const jobs = await res.json();
jobsDiv.innerHTML = jobs.map(j => `
<div class="job">
<h4>${j.title}</h4>
<p><strong>${j.company}</strong></p>
<p>${j.description || ''}</p>
</div>
`).join('');
}
// Handle new job submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
const title = document.getElementById('title').value;
const company = document.getElementById('company').value;
const description = document.getElementById('description').value;
await fetch('/api/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, company, description })
});
form.reset();
loadJobs();
});
// Initial load
loadJobs();
https://jobboard-yourusername.replit.app/api/jobs).
process.env.SECRET\_NAME.
This setup gives you a functioning, real job board app all inside Replit — with working backend, database, and frontend logic. You can now expand it by adding search filters, categories, or even authentication later using the same structure.
<script type="module">
import express from "express";
import Database from "@replit/database";
const app = express();
const db = new Database();
app.use(express.json());
// normalize strings for consistent search keys
const normalize = (str) => str.toLowerCase().trim();
// Create or update a job posting
app.post("/api/jobs", async (req, res) => {
const { id, title, company, description, tags = [] } = req.body;
if (!title || !company) return res.status(400).json({ error: "Missing fields" });
const jobId = id || crypto.randomUUID();
const job = { id: jobId, title, company, description, tags, createdAt: Date.now() };
await db.set(`job:${jobId}`, job);
// store search index by normalized tag string for quick filtering
for (const tag of tags.map(normalize)) {
const existing = (await db.get(`tag:${tag}`)) || [];
if (!existing.includes(jobId)) existing.push(jobId);
await db.set(`tag:${tag}`, existing);
}
res.status(200).json(job);
});
// Search jobs by tag
app.get("/api/jobs/search", async (req, res) => {
const tag = normalize(req.query.tag || "");
if (!tag) return res.status(400).json({ error: "Tag required" });
const jobIds = (await db.get(`tag:${tag}`)) || [];
const jobs = await Promise.all(jobIds.map((id) => db.get(`job:${id}`)));
res.json(jobs.filter(Boolean));
});
app.listen(3000, () => console.log("Job board API running on port 3000"));
</script>
<script type="module">
import express from "express";
import fetch from "node-fetch";
import Database from "@replit/database";
const app = express();
const db = new Database();
app.use(express.json());
// Automatically enrich job postings with company data from an external API (e.g. Clearbit)
app.post("/api/enrich-job", async (req, res) => {
const { title, company, description } = req.body;
if (!company) return res.status(400).json({ error: "Company field is required" });
try {
const apiKey = process.env.CLEARBIT\_KEY;
const response = await fetch(`https://company.clearbit.com/v2/companies/find?domain=${company}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const companyData = response.ok ? await response.json() : {};
const jobId = crypto.randomUUID();
const job = {
id: jobId,
title,
company,
description,
companyDomain: companyData.domain || company,
companyLogo: companyData.logo,
companyLocation: companyData.location,
createdAt: Date.now(),
};
await db.set(`job:${jobId}`, job);
res.status(200).json(job);
} catch (err) {
console.error("Error enriching job:", err);
res.status(500).json({ error: "Failed to enrich job data" });
}
});
app.listen(3000, () => console.log("Job board enrichment API running on port 3000"));
</script>
<script type="module">
import express from "express";
import Database from "@replit/database";
import crypto from "crypto";
const app = express();
const db = new Database();
app.use(express.json());
// Secure short-lived token for editing job posts
const TOKEN_TTL_MS = 5 _ 60 _ 1000; // 5 minutes
// Generate token for a specific jobId
app.post("/api/jobs/:id/token", async (req, res) => {
const { id } = req.params;
const job = await db.get(`job:${id}`);
if (!job) return res.status(404).json({ error: "Job not found" });
const token = crypto.randomBytes(16).toString("hex");
const expiry = Date.now() + TOKEN_TTL_MS;
await db.set(`editToken:${token}`, { jobId: id, expiry });
res.json({ token, expiresIn: TOKEN_TTL_MS / 1000 });
});
// Middleware to validate token
async function validateToken(req, res, next) {
const token = req.headers["x-edit-token"];
if (!token) return res.status(401).json({ error: "Missing token" });
const record = await db.get(`editToken:${token}`);
if (!record || record.expiry < Date.now())
return res.status(401).json({ error: "Invalid or expired token" });
req.jobId = record.jobId;
next();
}
// Update job only if token is valid
app.put("/api/jobs/secure-edit", validateToken, async (req, res) => {
const { title, description } = req.body;
const job = await db.get(`job:${req.jobId}`);
if (!job) return res.status(404).json({ error: "Job not found" });
const updated = { ...job, title: title || job.title, description: description || job.description, updatedAt: Date.now() };
await db.set(`job:${req.jobId}`, updated);
res.json(updated);
});
app.listen(3000, () => console.log("Job board token-secure API running on port 3000"));
</script>

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
To build a job board on Replit that actually works in real-world conditions — not just a prototype — use a Node.js + Express backend with a simple frontend (HTML/CSS/JS or React). Store job listings in a hosted database (like Replit’s built-in SQLite or an external PostgreSQL instance). The key best practices are: keep secrets (API keys, DB credentials) in the Secrets tab, organize code into clear folders, use environment variables, and use the Replit “Run” button smartly (don’t overload the main entrypoint). Local-style assumptions like file watchers or persistent local databases can bite you — Replit resets temporary files after restarts, so always use a persistent DB.
Create a new Repl using the Node.js template. This gives you a starting point with index.js.
routes/ to keep your route logic organized.models/ if you plan to manage database code or schema separately.express and sqlite3.
npm install express sqlite3
Inside your index.js file, set up your Express server. This is the main entry point when you click “Run”.
// index.js
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.use(express.json())
// Import routes
const jobsRouter = require('./routes/jobs')
app.use('/api/jobs', jobsRouter)
// Serve static files (for frontend)
app.use(express.static('public'))
app.listen(PORT, () => console.log(`Server running on port ${PORT}`))
Create a new file routes/jobs.js. This will handle job-related endpoints (posting and listing jobs).
// routes/jobs.js
const express = require('express')
const Router = express.Router()
const db = require('../models/db')
// List all jobs
Router.get('/', (req, res) => {
db.all('SELECT * FROM jobs', [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message })
res.json(rows)
})
})
// Add a new job
Router.post('/', (req, res) => {
const { title, company, description } = req.body
if (!title || !company) return res.status(400).json({ error: 'Missing fields' })
db.run('INSERT INTO jobs (title, company, description) VALUES (?, ?, ?)', [title, company, description], (err) => {
if (err) return res.status(500).json({ error: err.message })
res.status(201).json({ message: 'Job added successfully' })
})
})
module.exports = Router
Inside models/db.js, initialize SQLite (persistent in Replit). The database file is saved in your Repl and will persist unless deleted.
// models/db.js
const sqlite3 = require('sqlite3').verbose()
const db = new sqlite3.Database('jobs.db')
// Create table if it doesn't exist
db.run('CREATE TABLE IF NOT EXISTS jobs (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, company TEXT, description TEXT)')
module.exports = db
Create a public/ folder with index.html inside. Express’ static middleware will automatically serve it. Here’s a simple frontend to list and add jobs:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Job Board</title>
<meta charset="UTF-8" />
<script>
async function loadJobs() {
const res = await fetch('/api/jobs')
const jobs = await res.json()
document.getElementById('jobs').innerHTML = jobs.map(j => `<li><b>${j.title}</b> at ${j.company}</li>`).join('')
}
async function addJob() {
const title = document.getElementById('title').value
const company = document.getElementById('company').value
const description = document.getElementById('description').value
await fetch('/api/jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, company, description })
})
loadJobs()
}
window.onload = loadJobs
</script>
</head>
<body>
<h1>Job Board</h1>
<input id="title" placeholder="Job title" />
<input id="company" placeholder="Company name" />
<input id="description" placeholder="Description" />
<button onclick="addJob()">Add Job</button>
<ul id="jobs"></ul>
</body>
</html>
When you connect to external services (e.g., PostgreSQL, SendGrid API), store credentials in Replit’s Secrets tab (lock icon in sidebar). In your code, access them like:
const dbPassword = process.env.DB_PASSWORD
node index.js or sqlite3 jobs.db).
nodemon; Replit restarts your server automatically.process.env.PORT, don’t hardcode ports.
Follow this folder pattern and security setup, and your job board on Replit will be clean, persistent, shareable, and scalable for small production use or MVP testing. The key is treating Replit like a lightweight cloud dev environment — not as a local machine replica.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.