Learn how to build a powerful classifieds app using Replit. Follow this simple guide to create, deploy, and customize your own classifieds 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.
You can build a simple classifieds app on Replit using Node.js, Express, and a small JSON-based or Replit Database (key-value) storage. The workflow is: users can post ads (title, description, contact info), and others can browse or search them. Replit handles hosting, collaboration, and environment variables — so you can deploy and share your project easily. The simplest version can live entirely in one Repl. Use server.js as your backend entry file, public/ for frontend HTML/JS/CSS, and Replit’s built-in Database for persistence.
Start a new Repl using the Node.js template. Replit automatically gives you an environment with Node and npm ready. Inside the file tree on the left, you will see index.js by default. Rename it to server.js for clarity because it will be your Express server entry point.
npm install express
npm install @replit/database
In server.js, add this base code. It sets up a simple Express web server that can handle browser requests.
const express = require("express");
const app = express();
const Database = require("@replit/database");
const db = new Database();
const PORT = 3000;
// Allow Express to handle JSON sent from frontend forms
app.use(express.json());
app.use(express.static("public"));
// Basic home route
app.get("/", (req, res) => {
res.sendFile(__dirname + "/public/index.html");
});
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Now, make a public/ folder in the left sidebar. Inside it, create:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Replit Classifieds</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Classified Ads</h1>
<form id="adForm">
<input type="text" id="title" placeholder="Title" required><br>
<textarea id="description" placeholder="Description" required></textarea><br>
<input type="text" id="contact" placeholder="Contact Info" required><br>
<button type="submit">Post Ad</button>
</form>
<h2>All Listings</h2>
<div id="ads"></div>
<script src="script.js"></script>
</body>
</html>
// Fetch and show all ads
async function loadAds() {
const res = await fetch("/ads");
const ads = await res.json();
const list = document.getElementById("ads");
list.innerHTML = "";
ads.forEach(a => {
const item = document.createElement("div");
item.innerHTML = `<h3>${a.title}</h3><p>${a.description}</p><small>Contact: ${a.contact}</small>`;
list.appendChild(item);
});
}
document.getElementById("adForm").addEventListener("submit", async (e) => {
e.preventDefault();
const ad = {
title: document.getElementById("title").value,
description: document.getElementById("description").value,
contact: document.getElementById("contact").value
};
await fetch("/ads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ad)
});
loadAds(); // refresh after post
});
loadAds();
Back in server.js, under your other routes, add logic for creating and fetching ads. You’ll use Replit Database to store them persistently.
// Get all ads
app.get("/ads", async (req, res) => {
let ads = (await db.get("ads")) || [];
res.json(ads);
});
// Add new ad
app.post("/ads", async (req, res) => {
let ads = (await db.get("ads")) || [];
const newAd = {
title: req.body.title,
description: req.body.description,
contact: req.body.contact,
time: new Date().toISOString()
};
ads.push(newAd);
await db.set("ads", ads);
res.json({ success: true });
});
If later you add Admin routes or need an API key (for example, external email or image upload service), store them in Secrets (Environment Variables) by opening the padlock icon in the left sidebar. Then inside code you can access them by process.env.MY_SECRET_NAME.
You can later extend this with a search feature (filtering ads list in memory before render), add image uploads using a simple third-party service, or connect a full Postgres or MongoDB instance if the Replit Database becomes too simple. But the structure above is a real, functional classifieds platform that runs entirely on Replit’s free tier and demonstrates a true full-stack setup.
import express from "express";
import Database from "@replit/database";
const app = express();
const db = new Database();
app.use(express.json());
// Utility to generate unique IDs for listings
const genId = () => "listing\_" + Math.random().toString(36).substr(2, 9);
// POST /api/listings - create a new classified listing
app.post("/api/listings", async (req, res) => {
const { title, description, price, category, userId } = req.body;
if (!title || !price || !userId)
return res.status(400).json({ error: "Missing required fields" });
const id = genId();
const newListing = {
id,
title,
description: description || "",
price: parseFloat(price),
category: category || "general",
userId,
createdAt: Date.now(),
};
await db.set(id, newListing);
res.json({ success: true, listing: newListing });
});
// GET /api/listings?category=something - fetch listings by category
app.get("/api/listings", async (req, res) => {
const { category } = req.query;
const keys = await db.list("listing\_");
const allListings = await Promise.all(keys.map(key => db.get(key)));
const filtered = category
? allListings.filter(l => l.category === category)
: allListings;
res.json(filtered.sort((a, b) => b.createdAt - a.createdAt));
});
app.listen(3000, () => console.log("✅ Classifieds API running on port 3000"));
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());
// Store your API key safely in Replit Secrets as GEO_API_KEY
const GEO_API_URL = "https://api.mapbox.com/geocoding/v5/mapbox.places/";
app.post("/api/listings/with-location", async (req, res) => {
const { title, description, price, address, userId } = req.body;
if (!title || !price || !address || !userId)
return res.status(400).json({ error: "Missing required fields" });
try {
// Geocode address using external API (Mapbox in this example)
const geoRes = await fetch(
`${GEO_API_URL}${encodeURIComponent(address)}.json?access_token=${process.env.GEO_API_KEY}`
);
const geoData = await geoRes.json();
const coordinates = geoData.features?.[0]?.center || [0, 0];
const id = "listing\_" + Math.random().toString(36).substr(2, 9);
const listing = {
id,
title,
description: description || "",
price: parseFloat(price),
address,
location: { lng: coordinates[0], lat: coordinates[1] },
userId,
createdAt: Date.now(),
};
await db.set(id, listing);
res.json({ success: true, listing });
} catch (err) {
console.error("Error creating listing:", err);
res.status(500).json({ error: "Failed to geocode address" });
}
});
app.listen(3000, () => console.log("✅ Classifieds API with geolocation ready"));
import express from "express";
import multer from "multer";
import { createHash } from "crypto";
import Database from "@replit/database";
import fs from "fs";
const app = express();
const db = new Database();
const upload = multer({ dest: "uploads/" });
// Secure upload endpoint for classified listing images (stored by hash)
app.post("/api/upload", upload.single("file"), async (req, res) => {
if (!req.file) return res.status(400).json({ error: "No file uploaded" });
try {
// Generate hash for deduplication
const fileData = fs.readFileSync(req.file.path);
const hash = createHash("sha256").update(fileData).digest("hex");
const filename = `uploads/${hash}_${req.file.originalname}`;
// Avoid duplicates and keep storage minimal in Replit
if (!fs.existsSync(filename)) {
fs.renameSync(req.file.path, filename);
} else {
fs.unlinkSync(req.file.path);
}
// Store file metadata in the DB
const fileRecord = {
hash,
filename,
uploadedAt: Date.now(),
};
await db.set(`file_${hash}`, fileRecord);
res.json({
success: true,
fileUrl: `/images/${hash}`,
});
} catch (err) {
console.error("Upload error:", err);
res.status(500).json({ error: "Upload failed" });
}
});
// Serve uploaded images safely
app.get("/images/:hash", async (req, res) => {
const record = await db.get(`file_${req.params.hash}`);
if (!record) return res.status(404).send("Not found");
res.sendFile(record.filename, { root: "." });
});
app.listen(3000, () => console.log("✅ Upload API ready on port 3000"));

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 Classifieds app on Replit in a realistic, maintainable way, you’ll want to use a small Node.js + Express backend, a persistent database (like Replit’s built-in Replit Database or an external one like MongoDB Atlas), and a lightweight frontend that can be static HTML or React. Use Replit’s Secrets to keep private keys and DB URLs secure. Store user images using Replit’s file storage or external services. The most important best practices are: separate backend and frontend logic properly, don’t commit secrets, avoid blocking operations on the main thread, use the Replit shell and Console for logs, and test frequently by opening the webview link. Replit works best for small to midsize apps and MVPs — so optimize for simplicity and quick iteration rather than over-engineering.
Create these files and folders in your Replit project root:
// In the Replit Shell, install Express
npm install express
Open index.js (or create it if missing) and insert this code. It will handle basic routes like listing and posting ads.
// index.js
const express = require("express")
const path = require("path")
const db = require("./database") // we’ll create this next
const app = express()
const PORT = process.env.PORT || 3000
app.use(express.json()) // parse incoming JSON
app.use(express.static(path.join(__dirname, "public")))
// Route to list all classifieds
app.get("/api/classifieds", async (req, res) => {
const items = await db.getAll()
res.json(items)
})
// Route to post a new classified ad
app.post("/api/classifieds", async (req, res) => {
const { title, description, contact } = req.body
if (!title || !description) return res.status(400).json({ error: "Missing fields" })
const newItem = await db.insert({ title, description, contact })
res.json(newItem)
})
app.listen(PORT, () => console.log("Server running on port", PORT))
The simplest option inside Replit is Replit Database (@replit/database package). It’s lightweight, key-value based, and stored per Repl, so great for small apps and learning.
npm install @replit/database
Create a new file named database.js in the root folder and insert:
// database.js
const Database = require("@replit/database")
const db = new Database()
async function getAll() {
const keys = await db.list()
const records = []
for (const key of keys) {
const item = await db.get(key)
records.push(item)
}
return records
}
async function insert(item) {
const id = "ad_" + Date.now()
await db.set(id, { id, ...item })
return await db.get(id)
}
module.exports = { getAll, insert }
Create a public/index.html file. This will be your homepage listing ads and a simple form to post new ones:
<!DOCTYPE html>
<html>
<head>
<title>Replit Classifieds</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Replit Classifieds</h1>
<div id="ads"></div>
<h2>Post a new ad</h2>
<form id="adForm">
<input type="text" name="title" placeholder="Title" required /><br>
<textarea name="description" placeholder="Description" required></textarea><br>
<input type="text" name="contact" placeholder="Contact info" /><br>
<button type="submit">Post Ad</button>
</form>
<script>
async function loadAds() {
const res = await fetch("/api/classifieds")
const ads = await res.json()
document.getElementById("ads").innerHTML = ads.map(ad =>
`<div><h3>${ad.title}</h3><p>${ad.description}</p><small>${ad.contact || ""}</small></div>`
).join("")
}
document.getElementById("adForm").addEventListener("submit", async (e) => {
e.preventDefault()
const form = e.target
const ad = {
title: form.title.value,
description: form.description.value,
contact: form.contact.value
}
await fetch("/api/classifieds", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ad)
})
form.reset()
loadAds()
})
loadAds()
</script>
</body>
</html>
If you connect to external APIs or databases, never hardcode credentials. In Replit, click the “🔒 Secrets” tab on the left, add keys, and access them via process.env.KEY\_NAME in Node.js.
// Example to use MongoDB Atlas
const MONGO_URI = process.env.MONGO_URI
console.log() to debug. The console output helps trace backend issues fast.
Replit is great for prototyping and small-project hosting, but for a full classifieds marketplace:
Following this approach keeps your Classifieds app manageable in Replit — clean separation of frontend, backend, and data, secure handling of secrets, and easy debugging in the Replit environment.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.