/how-to-build-replit

How to Build a Habit tracker with Replit

Learn how to build a habit tracker with Replit. Follow simple steps to code, track progress, and boost productivity through consistent daily habits.

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 Habit tracker with Replit

A simple and realistic way to build a Habit Tracker on Replit is to use a small Express.js backend (Node.js) to store your habits, a lightweight SQLite database file for persistence, and a minimal HTML + JS frontend to show and update habits. You can set this up directly inside one Repl, deploy it instantly, and even use Replit’s built-in Database or external SQLite depending on your comfort level. This approach works smoothly on Replit’s free tier and mirrors how real full-stack apps are structured.

 

Set up the Project

 

  • Create a new Repl → choose Node.js template.
  • Inside the Replit file tree, you’ll see a default index.js file — that will be your server file.
  • Create a new folder called public. Inside it, add a new file named index.html. That will hold your frontend UI.
  • In the Shell tab (bottom panel), install dependencies:
npm install express better-sqlite3

Express handles your backend routes. better-sqlite3 is a fast, simple SQLite library that works perfectly on Replit.

 

Build the Backend (index.js)

 

In index.js (replace all the content with this):

// Load dependencies
const express = require('express');
const Database = require('better-sqlite3');
const path = require('path');

// Create Express app
const app = express();

// Middlewares
app.use(express.json());
app.use(express.static('public')); // serve the /public folder

// Initialize SQLite database file in Replit environment
const db = new Database('habits.db');

// Create table if doesn't exist
db.prepare(`
  CREATE TABLE IF NOT EXISTS habits (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT,
    count INTEGER
  )
`).run();

// Route: get all habits
app.get('/api/habits', (req, res) => {
  const rows = db.prepare('SELECT * FROM habits').all();
  res.json(rows);
});

// Route: add habit
app.post('/api/habits', (req, res) => {
  const { name } = req.body;
  db.prepare('INSERT INTO habits (name, count) VALUES (?, ?)').run(name, 0);
  res.json({ success: true });
});

// Route: increment habit count
app.post('/api/habits/:id/increment', (req, res) => {
  const { id } = req.params;
  db.prepare('UPDATE habits SET count = count + 1 WHERE id = ?').run(id);
  res.json({ success: true });
});

// Start server
app.listen(3000, () => {
  console.log('Habit Tracker running on http://localhost:3000');
});

This script will create a small HTTP server that lets you list, add, and increment habits. On Replit, this automatically starts when you click “Run”. You’ll see a “🎧 Listening” URL in the webview panel.

 

Build the Frontend (public/index.html)

 

Now open public/index.html and add this:

<!DOCTYPE html>
<html>
  <head>
    <title>Habit Tracker</title>
    <meta charset="UTF-8" />
  </head>
  <body style="font-family: sans-serif; max-width: 600px; margin: 2rem auto;">
    <h1>Habit Tracker</h1>

    <form id="habitForm">
      <input type="text" id="habitName" placeholder="New habit..." required />
      <button>Add Habit</button>
    </form>

    <ul id="habitList"></ul>

    <script>
      const habitForm = document.getElementById('habitForm');
      const habitList = document.getElementById('habitList');

      // Load habits on page load
      async function loadHabits() {
        const res = await fetch('/api/habits');
        const habits = await res.json();
        habitList.innerHTML = '';
        habits.forEach(habit => {
          const li = document.createElement('li');
          li.innerHTML = `${habit.name}: ${habit.count} 
                          <button onclick="increment(${habit.id})">+1</button>`;
          habitList.appendChild(li);
        });
      }

      async function increment(id) {
        await fetch('/api/habits/' + id + '/increment', { method: 'POST' });
        loadHabits();
      }

      habitForm.addEventListener('submit', async e => {
        e.preventDefault();
        const name = document.getElementById('habitName').value;
        await fetch('/api/habits', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ name })
        });
        document.getElementById('habitName').value = '';
        loadHabits();
      });

      loadHabits();
    </script>
  </body>
</html>

This frontend uses simple HTML and JavaScript to interact with the backend API. The page lists your habits, allows adding new ones, and lets you click “+1” to log progress.

 

Run and Test

 

  • Click the green Run button at the top of Replit.
  • After a few seconds, a small preview window opens on the right. You’ll see your Habit Tracker UI. Try adding a few habits and incrementing them.
  • Replit automatically assigns a web URL (you can open it with the “Open in new tab” icon next to the preview).

Your data will persist because SQLite writes a local file (habits.db) inside your Repl. If you restart the Repl, habits stay saved as long as you don’t delete that file.

 

Practical Tips for Replit Users

 

  • Don’t commit your database to Git — it changes often. Add habits.db to .gitignore if you push to GitHub.
  • Use Replit Secrets (on the “🔒 Secrets” tab) if later you add API keys or credentials.
  • If you ever get a port error, make sure the app listens on process.env.PORT instead of 3000 — Replit needs that for some deployments.
  • If you switch to Replit’s built-in key/value database instead of SQLite, you can use @replit/database package, which is simpler but less structured.
  • Deploy: For production use, go to the “Deployments” tab → “Static or Always On” depending on your plan.

 

This approach is 100% real, works fully in Replit, and stays faithful to how professional full-stack apps are built there — simple, modular, and resilient to Replit’s environment quirks.

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 Build a Simple Habit Tracker API with Express and Replit Database



import express from "express"
import Database from "@replit/database"

const app = express()
const db = new Database()

app.use(express.json())

// Structure habits by user -> date -> habit-name: status
app.post("/api/habits/update", async (req, res) => {
  const { userId, date, habit, done } = req.body
  if (!userId || !date || !habit)
    return res.status(400).json({ error: "Missing required fields" })

  const userHabits = (await db.get(userId)) || {}
  const dayData = userHabits[date] || {}
  dayData[habit] = done
  userHabits[date] = dayData

  await db.set(userId, userHabits)
  res.json({ success: true })
})

app.get("/api/habits/:userId/:date", async (req, res) => {
  const { userId, date } = req.params
  const userData = (await db.get(userId)) || {}
  res.json(userData[date] || {})
})

app.listen(3000, () => console.log("Habit Tracker API running on port 3000"))

How to Send Daily Habit Summary Emails with Mailgun in Your Replit Habit Tracker



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

// Example: Send daily habit completion summary email via Mailgun API
app.post("/api/notify/daily-summary", async (req, res) => {
  const { userId, email } = req.body
  if (!userId || !email) return res.status(400).json({ error: "Missing userId or email" })

  const userData = (await db.get(userId)) || {}
  const today = new Date().toISOString().split("T")[0]
  const todayHabits = userData[today] || {}

  const completed = Object.keys(todayHabits).filter(h => todayHabits[h])
  const missed = Object.keys(todayHabits).filter(h => !todayHabits[h])

  const text = `Your habits for ${today}:\nCompleted: ${completed.join(", ") || "None"}\nMissed: ${missed.join(", ") || "None"}`

  const mgDomain = process.env.MG\_DOMAIN
  const mgKey = process.env.MG_API_KEY

  const response = await fetch(`https://api.mailgun.net/v3/${mgDomain}/messages`, {
    method: "POST",
    headers: {
      "Authorization": "Basic " + Buffer.from("api:" + mgKey).toString("base64"),
      "Content-Type": "application/x-www-form-urlencoded"
    },
    body: new URLSearchParams({
      from: `Habit Tracker <no-reply@${mgDomain}>`,
      to: email,
      subject: "Your Daily Habit Summary",
      text
    })
  })

  if (response.ok) return res.json({ success: true })
  const errText = await response.text()
  res.status(500).json({ error: "Email failed", detail: errText })
})

app.listen(3000, () => console.log("Habit Tracker with Mailgun notifications running on port 3000"))

How to Automatically Reset Daily Habits with Node.js and Replit Database



import express from "express"
import Database from "@replit/database"
import cron from "node-cron"

const app = express()
const db = new Database()

app.use(express.json())

// Automatically reset daily habits at midnight (UTC)
cron.schedule("0 0 _ _ \*", async () => {
  const keys = await db.list()
  for (const userId of keys) {
    const userData = (await db.get(userId)) || {}
    const today = new Date().toISOString().split("T")[0]
    // Copy yesterday's habits but reset completion to false
    const lastDayKey = Object.keys(userData).sort().pop()
    const lastDayHabits = lastDayKey ? userData[lastDayKey] : {}
    const resetHabits = Object.keys(lastDayHabits).reduce((a, h) => {
      a[h] = false
      return a
    }, {})
    userData[today] = resetHabits
    await db.set(userId, userData)
  }
  console.log("Daily habit reset completed.")
})

// Endpoint to trigger manual reset (for debugging)
app.post("/admin/reset-now", async (req, res) => {
  await (async () => {
    const keys = await db.list()
    for (const userId of keys) {
      const userData = (await db.get(userId)) || {}
      const today = new Date().toISOString().split("T")[0]
      userData[today] = {}
      await db.set(userId, userData)
    }
  })()
  res.json({ success: true, message: "Manual reset done." })
})

app.listen(3000, () => console.log("Habit Tracker daily reset service 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 Habit tracker with Replit

When building a habit tracker on Replit, the most reliable setup is to use a simple Node.js + Express server with a lightweight SQLite database (through better-sqlite3 or sqlite package) and a React or vanilla JS frontend. Replit gives you an always-on web server with instant deployment, but you must manage data persistence carefully — local .db files are fine for small projects, but for real users or team projects, connect to an external database (like Replit DB, Supabase, or MongoDB Atlas). Always store API keys in the “Secrets” tab, never hardcode them. Your code should separate the backend and frontend clearly, and take advantage of Replit’s multiplayer feature for collaboration.

 

Project Structure

 

Create your Repl using the “Node.js” template. Then organize your files like this:

habit-tracker/
├── index.js              // main backend file (Express server)
├── public/
│    ├── index.html       // main HTML
│    ├── style.css        // basic styling
│    └── script.js        // frontend interaction logic
├── habit.db              // SQLite database (auto-created)
├── package.json          // dependencies

 

Setup the Backend (index.js)

 

The backend handles data storage and API routes. Install dependencies in Replit’s Shell (bottom pane):

npm install express better-sqlite3

Now, inside index.js, paste this code:

const express = require('express')
const Database = require('better-sqlite3')
const app = express()
const db = new Database('habit.db')

// Middleware to parse JSON
app.use(express.json())
app.use(express.static('public'))

// Create table if it doesn't exist
db.prepare('CREATE TABLE IF NOT EXISTS habits (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, done INTEGER)').run()

// API to get all habits
app.get('/api/habits', (req, res) => {
  const habits = db.prepare('SELECT * FROM habits').all()
  res.json(habits)
})

// API to add new habit
app.post('/api/habits', (req, res) => {
  const { name } = req.body
  if(!name) return res.status(400).json({error: 'Name required'})
  db.prepare('INSERT INTO habits (name, done) VALUES (?, 0)').run(name)
  res.json({success: true})
})

// API to mark habit done
app.post('/api/habits/:id/done', (req, res) => {
  const { id } = req.params
  db.prepare('UPDATE habits SET done = 1 WHERE id = ?').run(id)
  res.json({success: true})
})

const port = process.env.PORT || 3000
app.listen(port, () => console.log('Server running on port', port))

This creates a simple REST API and static file server. On Replit, it auto-exposes the public URL when you click “Run”.

 

Frontend (public/index.html)

 

This file displays habits and allows adding/checking them. Create a new file under public/ named index.html and add:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Habit Tracker</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <h1>My Habit Tracker</h1>
  <input id="habitName" placeholder="Add new habit..." />
  <button id="addHabitBtn">Add Habit</button>

  <ul id="habitList"></ul>

  <script src="script.js"></script>
</body>
</html>

 

Frontend Script (public/script.js)

 

This file communicates with your Express API. Create it under public/script.js:

async function fetchHabits() {
  const res = await fetch('/api/habits')
  const habits = await res.json()
  const list = document.getElementById('habitList')
  list.innerHTML = ''
  habits.forEach(h => {
    const li = document.createElement('li')
    li.textContent = h.name + (h.done ? ' ✅' : '')
    if(!h.done) {
      const btn = document.createElement('button')
      btn.textContent = 'Mark Done'
      btn.onclick = () => markDone(h.id)
      li.appendChild(btn)
    }
    list.appendChild(li)
  })
}

async function addHabit() {
  const name = document.getElementById('habitName').value
  if(!name.trim()) return
  await fetch('/api/habits', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({name})
  })
  document.getElementById('habitName').value = ''
  fetchHabits()
}

async function markDone(id) {
  await fetch(`/api/habits/${id}/done`, {method: 'POST'})
  fetchHabits()
}

document.getElementById('addHabitBtn').onclick = addHabit
fetchHabits()

 

Styling (public/style.css)

 

Create a public/style.css file for simple UI:

body {
  font-family: system-ui;
  margin: 40px;
}
button {
  margin-left: 10px;
}

 

Replit Environment and Best Practices

 

  • Secrets: Go to the “Tools” sidebar → “Secrets”. Store API keys or database URLs there. Access them via process.env.VARIABLE\_NAME in Node.
  • Persistence: Replit keeps your SQLite file persistent, but deleting the Repl or resetting it will lose data. For production, use external DB (Supabase, MongoDB Atlas, etc.).
  • Live reload: Replit auto restarts your app when files change. Avoid creating infinite logs (e.g., in loops), it can freeze Replit shell.
  • Testing: You can use “Shell” to run curl commands for quick API testing: curl -X POST -H "Content-Type: application/json" -d '{"name":"Read"}' https://your-repl-url/api/habits.
  • Multiplayer: Invite teammates via “Invite” button → collaborate live in code and shell. Great for pair-debugging or adding visual polish together.
  • Git Integration: Use “Version Control” sidebar to connect GitHub repo, track changes, and restore previous states safely.

 

Common Pitfalls

 

  • Do not rely on localhost URLs — Replit assigns its own public URL when running your server.
  • Never hardcode secrets in JS files; always use the Secrets tab.
  • Avoid heavy dependencies — Replit has limited long-term memory and CPU; keep it lightweight.
  • SQLite file locks can occur if multiple requests write simultaneously. For small apps, it’s fine, but if scaling, move to a hosted DB.

 

This exact setup gives you a working, persistent habit tracker inside Replit’s environment. It uses real, proven packages (Express + better-sqlite3), respects Replit’s file structure, and can be extended easily — for example, adding user login with external DB or adding analytics.

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