Learn how to build a powerful task management app with Replit. Follow step-by-step coding tips to create, organize, and manage tasks efficiently.

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 task management app on Replit, create a simple full-stack project using Node.js with Express.js for the backend and HTML/CSS/JS for the frontend. The backend will manage tasks (storing and reading from a local database file like tasks.json), and the frontend will send API calls to it. You can later switch this local JSON “database” to a persistent one (like Replit’s Replit Database or PostgreSQL) when you’re ready to scale. Replit will handle hosting automatically when you run the app, and you can expose environment variables (like API keys) via the Secrets tab.
When starting from scratch, open Replit, click + Create, choose Node.js template, and name it task-manager. Replit will automatically create a file index.js where your main server logic goes.
Inside your Replit shell (the console below your editor), install Express:
npm install express
Then replace the default content of index.js with this code:
// index.js
const express = require('express')
const fs = require('fs')
const cors = require('cors')
const app = express()
const PORT = process.env.PORT || 3000
app.use(cors()) // allows frontend to talk to backend
app.use(express.json()) // parses incoming JSON
// Load tasks from file or create empty array
let tasks = []
const dataFile = './tasks.json'
if (fs.existsSync(dataFile)) {
const fileData = fs.readFileSync(dataFile)
tasks = JSON.parse(fileData)
}
// GET all tasks
app.get('/api/tasks', (req, res) => {
res.json(tasks)
})
// POST new task
app.post('/api/tasks', (req, res) => {
const newTask = { id: Date.now(), text: req.body.text, done: false }
tasks.push(newTask)
fs.writeFileSync(dataFile, JSON.stringify(tasks, null, 2))
res.json(newTask)
})
// PUT toggle task done
app.put('/api/tasks/:id', (req, res) => {
const id = parseInt(req.params.id)
const task = tasks.find(t => t.id === id)
if (!task) return res.status(404).json({ error: 'Task not found' })
task.done = !task.done
fs.writeFileSync(dataFile, JSON.stringify(tasks, null, 2))
res.json(task)
})
// DELETE task
app.delete('/api/tasks/:id', (req, res) => {
const id = parseInt(req.params.id)
tasks = tasks.filter(t => t.id !== id)
fs.writeFileSync(dataFile, JSON.stringify(tasks, null, 2))
res.json({ message: 'Task deleted' })
})
// Serve frontend files
app.use(express.static('public'))
app.listen(PORT, () => console.log('Server running on port', PORT))
In the left sidebar, create a new folder called public. Inside it, create these three files: index.html, script.js, and style.css.
Now add this content to public/index.html:
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Task Manager</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>My Tasks</h1>
<div id="tasks"></div>
<input id="newTask" placeholder="Add new task..." />
<button id="addBtn">Add</button>
<script src="script.js"></script>
</body>
</html>
Then add this to public/style.css:
/* public/style.css */
body {
font-family: sans-serif;
margin: 30px;
background: #f9f9f9;
}
h1 {
color: #333;
}
.task {
display: flex;
align-items: center;
justify-content: space-between;
background: white;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.task.done {
text-decoration: line-through;
color: gray;
}
And finally add this to public/script.js:
// public/script.js
const tasksDiv = document.getElementById('tasks')
const newTaskInput = document.getElementById('newTask')
const addBtn = document.getElementById('addBtn')
async function fetchTasks() {
const res = await fetch('/api/tasks')
const data = await res.json()
renderTasks(data)
}
function renderTasks(list) {
tasksDiv.innerHTML = ''
list.forEach(task => {
const div = document.createElement('div')
div.className = 'task' + (task.done ? ' done' : '')
div.textContent = task.text
div.onclick = async () => {
await fetch(`/api/tasks/${task.id}`, { method: 'PUT' })
fetchTasks()
}
const delBtn = document.createElement('button')
delBtn.textContent = 'x'
delBtn.onclick = async (e) => {
e.stopPropagation() // don't toggle done
await fetch(`/api/tasks/${task.id}`, { method: 'DELETE' })
fetchTasks()
}
div.appendChild(delBtn)
tasksDiv.appendChild(div)
})
}
addBtn.onclick = async () => {
const text = newTaskInput.value.trim()
if (!text) return
await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
})
newTaskInput.value = ''
fetchTasks()
}
fetchTasks()
Click Run ▶️ at the top. Replit automatically runs your Node server and opens a web preview window with your frontend. You can now add, toggle, and delete tasks. Each action updates tasks.json stored on Replit’s filesystem.
If the webview doesn’t refresh properly, open the “Open in new tab” icon next to the web preview URL (it’s often clearer for debugging CORS issues).
This setup gives you a working, persistent (within Replit’s file sandbox) full-stack Task Manager app — built, run, and hosted entirely in Replit.
import express from "express";
import Database from "@replit/database";
import { nanoid } from "nanoid";
const app = express();
const db = new Database();
app.use(express.json());
// Create or update a user's task
app.post("/api/tasks", async (req, res) => {
const { userId, title, dueDate, status } = req.body;
if (!userId || !title) return res.status(400).json({ error: "Missing fields" });
const taskId = nanoid();
const newTask = { id: taskId, title, dueDate: dueDate || null, status: status || "pending" };
const userTasks = (await db.get(`tasks_${userId}`)) || [];
userTasks.push(newTask);
await db.set(`tasks_${userId}`, userTasks);
res.json({ success: true, task: newTask });
});
// Fetch tasks for a user
app.get("/api/tasks/:userId", async (req, res) => {
const { userId } = req.params;
const tasks = (await db.get(`tasks_${userId}`)) || [];
res.json(tasks);
});
// Update a task's status or details
app.put("/api/tasks/:userId/:taskId", async (req, res) => {
const { userId, taskId } = req.params;
const updatedFields = req.body;
const userTasks = (await db.get(`tasks_${userId}`)) || [];
const updatedTasks = userTasks.map((t) =>
t.id === taskId ? { ...t, ...updatedFields } : t
);
await db.set(`tasks_${userId}`, updatedTasks);
res.json({ success: true });
});
// Replit environment port handling
app.listen(process.env.PORT || 3000, () => console.log("Server running"));
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());
// Sync local tasks with external project management API (for example, Notion or Trello)
app.post("/api/sync", async (req, res) => {
const { userId } = req.body;
if (!userId) return res.status(400).json({ error: "User ID required" });
const tasks = (await db.get(`tasks_${userId}`)) || [];
const externalApiKey = process.env.EXTERNAL_API_KEY;
const externalProjectId = process.env.EXTERNAL_PROJECT_ID;
try {
const syncResults = await Promise.all(
tasks.map(async (task) => {
const response = await fetch(`https://api.external-service.com/v1/projects/${externalProjectId}/tasks`, {
method: "POST",
headers: {
"Authorization": `Bearer ${externalApiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
name: task.title,
due: task.dueDate,
status: task.status,
metadata: { localId: task.id }
}),
});
if (!response.ok) throw new Error(`Failed to sync task ${task.id}`);
const data = await response.json();
return { localId: task.id, externalId: data.id };
})
);
await db.set(`sync_${userId}`, syncResults);
res.json({ success: true, syncedCount: syncResults.length });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to sync tasks" });
}
});
app.listen(process.env.PORT || 3000, () => console.log("Sync service running"));
import express from "express";
import Database from "@replit/database";
import nodemailer from "nodemailer";
const app = express();
const db = new Database();
app.use(express.json());
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.NOTIFY\_EMAIL,
pass: process.env.NOTIFY\_PASS
}
});
// Checks for overdue tasks and emails the user summary
app.post("/api/notify-overdue", async (req, res) => {
const { userId, userEmail } = req.body;
if (!userId || !userEmail) return res.status(400).json({ error: "Missing user info" });
const tasks = (await db.get(`tasks_${userId}`)) || [];
const now = new Date();
const overdue = tasks.filter(t => t.dueDate && new Date(t.dueDate) < now && t.status !== "done");
if (overdue.length === 0) return res.json({ message: "No overdue tasks" });
const message = {
from: process.env.NOTIFY\_EMAIL,
to: userEmail,
subject: "Overdue Task Reminder",
text: `You have ${overdue.length} overdue task(s):\n\n${overdue.map(t => `- ${t.title} (due ${t.dueDate})`).join("\n")}`
};
try {
await transporter.sendMail(message);
res.json({ success: true, sent: overdue.length });
} catch (err) {
console.error(err);
res.status(500).json({ error: "Failed to send email" });
}
});
app.listen(process.env.PORT || 3000, () => console.log("Notifier running"));

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
The best practice for building a task management app on Replit is to keep the architecture simple but structured: use Node.js with Express for the backend API, SQLite or Replit’s built-in Database for quick persistence, and React (or EJS templates if you stay server-rendered) for the frontend. Always use Secrets for sensitive data, commit your code to GitHub for versioning, and use pinned Replit packages (by version) to avoid breaking updates. Organize your project files into clearly separated folders — for example, server/ for backend logic, client/ for frontend, and avoid dumping everything in the root. Test early in the Replit “Shell” tab, use the built-in Console for debugging logs, and never rely on Replit’s temporary filesystem to store task data unless you deliberately choose the Replit Database.
Inside Replit, create a Node.js Repl. Then structure your folders manually in the left-hand pane:
server/ — Express backend (handles tasks CRUD)client/ — React app (or static HTML+JS front)db.js — database connection or Replit DB logicindex.js — entry point for ExpressExample folder structure:
.
├── index.js
├── db.js
├── server/
│ └── routes.js
└── client/
├── index.html
├── app.js
└── styles.css
Open index.js in the root folder. This file starts your web server. You can paste the following code there:
import express from "express"
import bodyParser from "body-parser"
import cors from "cors"
import { tasksRouter } from "./server/routes.js" // we’ll create this next
const app = express()
const PORT = process.env.PORT || 3000
app.use(cors())
app.use(bodyParser.json())
app.use("/api/tasks", tasksRouter)
// Serve client files
app.use(express.static("client"))
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
Explanation: This code sets up your main server. express.static("client") tells Express to serve your frontend (the client/ folder). app.use("/api/tasks", tasksRouter) mounts our API handler for tasks (we’ll build that next).
In Replit, you can use either @replit/database or SQLite. For simplicity and reliability in Replit’s file system, use Replit DB. Create a new file named db.js in the root.
import Database from "@replit/database"
export const db = new Database()
// Example function you can reuse
export async function getAllTasks() {
const keys = await db.list()
const allTasks = []
for (let key of keys) {
const value = await db.get(key)
allTasks.push({ id: key, ...value })
}
return allTasks
}
This file will connect to Replit’s lightweight key-value store. Data persists across sessions, but is still simpler than a full SQL database.
Inside server/routes.js, create routes for adding, reading, and deleting tasks:
import express from "express"
import { db, getAllTasks } from "../db.js"
export const tasksRouter = express.Router()
// Get all tasks
tasksRouter.get("/", async (req, res) => {
const tasks = await getAllTasks()
res.json(tasks)
})
// Add new task
tasksRouter.post("/", async (req, res) => {
const { title } = req.body
if (!title) {
return res.status(400).json({ error: "Title is required" })
}
const id = Date.now().toString()
const newTask = { title, done: false }
await db.set(id, newTask)
res.json({ id, ...newTask })
})
// Mark as done or delete
tasksRouter.delete("/:id", async (req, res) => {
const id = req.params.id
await db.delete(id)
res.json({ success: true })
})
This router provides the backend logic for your app. Note the async/await calls, which are necessary because Replit DB is asynchronous.
Create client/index.html that loads the front UI. The frontend fetches from /api/tasks and renders the results dynamically. You can use vanilla JS or React. Below is a simple example using vanilla JS to keep things straightforward.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Task Manager</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>Task Manager</h1>
<form id="task-form">
<input id="task-input" placeholder="Add a new task" required />
<button type="submit">Add</button>
</form>
<ul id="task-list"></ul>
<script src="app.js"></script>
</body>
</html>
Now create client/app.js to connect backend and frontend:
const taskList = document.getElementById("task-list")
const taskForm = document.getElementById("task-form")
const taskInput = document.getElementById("task-input")
async function loadTasks() {
const res = await fetch("/api/tasks")
const tasks = await res.json()
taskList.innerHTML = ""
tasks.forEach((t) => {
const li = document.createElement("li")
li.textContent = t.title
const delBtn = document.createElement("button")
delBtn.textContent = "Delete"
delBtn.onclick = async () => {
await fetch(`/api/tasks/${t.id}`, { method: "DELETE" })
loadTasks()
}
li.appendChild(delBtn)
taskList.appendChild(li)
})
}
taskForm.onsubmit = async (e) => {
e.preventDefault()
await fetch("/api/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: taskInput.value })
})
taskInput.value = ""
loadTasks()
}
loadTasks()
This simple script connects to your API, fetches tasks, renders them, and allows adding/deleting tasks — fully functional in Replit.
Open the left sidebar in Replit → click the “lock” icon labeled Secrets. Put any credentials there (for example, if you later connect external database). Then in your code, access them via process.env.MY_SECRET_NAME. Never hardcode sensitive information directly into files that sync to GitHub.
npm install or check logs with echo instead of the Console when debugging deeper issues.package.json to avoid breaking changes when Replit auto-updates packages.
Following this pattern gives you a reliable, maintainable, Replit-friendly foundation. You’ll have a clean frontend, backend, and DB separation, proper data persistence, and a workflow that won’t suddenly break due to how Replit containers work. This is exactly how to build a small but production-quality task management app inside Replit.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.