/how-to-build-replit

How to Build a Task management app with Replit

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

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

How to Build a Task management app with Replit

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.

 

Step 1: Create your Repl with Node.js

 

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.

 

Step 2: Setup Express and basic server

 

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))

 

Step 3: Create your frontend folder and files

 

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()

 

Step 4: Run and test on Replit

 

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).

 

Step 5: Persist and share

 

  • You can commit your Repl to GitHub using Replit’s Version Control sidebar, so you have proper version history.
  • If you need persistent data across deployments, switch from tasks.json to Replit Database — accessible by installing `npm install @replit/database` and updating code to use it instead of the filesystem.
  • For team collaboration, invite other users at the top-right corner (the “Invite” button). You’ll see real-time collaboration just like Google Docs.

 

Common pitfalls on Replit

 

  • Filesystem resets: Sometimes if a Repl restarts or runs out of storage, files can get cleared. Always back up using Git!
  • Long-running background tasks: Free-tier Repls sleep after inactivity. Use Replit Deployments if you need 24/7 uptime.
  • Secrets: Never store API keys in your code; put them under the padlock icon in “Secrets”. Access them in code via `process.env.MY_KEY`.

 

This setup gives you a working, persistent (within Replit’s file sandbox) full-stack Task Manager app — built, run, and hosted entirely in Replit.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Contact Us

How to Create and Manage Tasks with Express and Replit Database



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"));

How to Sync Your Tasks with an External API in Your Replit Task Management App



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"));

How to Send Automated Overdue Task Email Notifications in Your Replit App



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"));

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Contact Us
Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Best Practices for Building a Task management app with Replit

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.

 

Project Structure

 

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 logic
  • index.js — entry point for Express

Example folder structure:

.
├── index.js
├── db.js
├── server/
│   └── routes.js
└── client/
    ├── index.html
    ├── app.js
    └── styles.css

 

Setting Up the Express Server

 

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).

 

Creating a Simple Database File

 

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.

 

Creating the Routes

 

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.

 

Frontend (Client Side)

 

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>

 

Frontend Logic

 

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.

 

Using Replit Secrets

 

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.

 

Common Replit-Specific Best Practices

 

  • Always commit changes frequently — Replit autosave is great, but explicit commits help when things break.
  • Don’t store uploaded files in /tmp or random paths — Replit VMs reset; use Replit DB or external storage like Supabase if you need persistence.
  • Use the Shell tab to run npm install or check logs with echo instead of the Console when debugging deeper issues.
  • Pin package versions in your package.json to avoid breaking changes when Replit auto-updates packages.
  • Test routes manually via the “Webview” or copy the URL and call it from your browser or Postman, to confirm your backend/API works before hooking frontend logic.

 

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.

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022