Learn how to build an event calendar app using Replit. Follow this step-by-step guide to create, customize, and deploy your own web app easily.

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 simplest valid and scalable way to build an event calendar app on Replit is to use a Node.js + Express backend with a small HTML/CSS/JS frontend. The backend will store events (you can start with in-memory storage, then move to a Replit Database). The frontend will display and allow adding events using simple forms. Replit makes this easy to run in one environment — your frontend can live in the same Repl as your backend, and you can deploy it directly using the “Run” button.
When you create a new Repl, choose the Node.js template. You’ll start with a main file named index.js.
In Replit, all server code (Express, APIs, etc.) goes into index.js by default. All static files (HTML, CSS, JS for the frontend) should live inside a public folder you’ll create manually.
Open the Shell tab at the bottom of Replit and run:
npm install express
This installs Express, the lightweight web server library Node apps use. After installation, open your index.js file and replace its content with:
// index.js
const express = require("express")
const app = express()
const port = 3000
// Allow Express to read JSON and form data in requests
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// Serve static files from the "public" folder
app.use(express.static("public"))
// Temporary in-memory storage for events
let events = []
// GET endpoint to fetch all events
app.get("/api/events", (req, res) => {
res.json(events)
})
// POST endpoint to add a new event
app.post("/api/events", (req, res) => {
const { title, date, description } = req.body
// Basic validation
if (!title || !date) return res.status(400).send("Missing title or date")
const newEvent = { id: Date.now(), title, date, description: description || "" }
events.push(newEvent)
res.status(201).json(newEvent)
})
app.listen(port, () => {
console.log(`Server running on http://localhost:${port}`)
})
This code defines an API for your frontend to retrieve and add events. Right now, it only stores events in memory — they reset whenever you restart the Repl. We'll handle persistence in a later step.
In your Replit file explorer, click the + New Folder icon, name it public. Inside it, create these files:
Paste this inside public/index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Event Calendar</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Event Calendar</h1>
<form id="event-form">
<input type="text" id="title" placeholder="Event title" required />
<input type="date" id="date" required />
<textarea id="description" placeholder="Description"></textarea>
<button type="submit">Add Event</button>
</form>
<div id="events"></div>
<script src="script.js"></script>
</body>
</html>
Then, inside public/style.css, paste this for simple styling:
body {
font-family: sans-serif;
margin: 20px;
background: #f5f5f5;
}
form {
margin-bottom: 20px;
}
#events {
background: white;
border-radius: 5px;
padding: 10px;
}
.event {
border-bottom: 1px solid #ccc;
padding: 5px 0;
}
Now, add logic in public/script.js to fetch and submit events:
// public/script.js
async function loadEvents() {
const res = await fetch("/api/events")
const events = await res.json()
const container = document.getElementById("events")
container.innerHTML = ""
events.forEach(ev => {
const div = document.createElement("div")
div.classList.add("event")
div.innerHTML = `<strong>${ev.title}</strong> - ${ev.date}<br/>${ev.description}`
container.appendChild(div)
})
}
document.getElementById("event-form").addEventListener("submit", async (e) => {
e.preventDefault()
const title = document.getElementById("title").value
const date = document.getElementById("date").value
const description = document.getElementById("description").value
await fetch("/api/events", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, date, description })
})
document.getElementById("event-form").reset()
loadEvents()
})
// Load events when page loads
loadEvents()
Now click “Run” in Replit. Your app should open with a form to add events and a list to display them dynamically.
Replit has a built-in lightweight key-value store called the Replit Database. It’s simple to use for prototypes like this. To use it, install this package in the Shell:
npm install @replit/database
Then edit your index.js — replace the in-memory code for storing events with persistent storage:
// add near top after requires
const Database = require("@replit/database")
const db = new Database()
// Instead of 'let events = []', remove that line
// Update the GET route
app.get("/api/events", async (req, res) => {
const events = (await db.get("events")) || []
res.json(events)
})
// Update the POST route
app.post("/api/events", async (req, res) => {
const { title, date, description } = req.body
if (!title || !date) return res.status(400).send("Missing title or date")
const events = (await db.get("events")) || []
const newEvent = { id: Date.now(), title, date, description: description || "" }
events.push(newEvent)
await db.set("events", events)
res.status(201).json(newEvent)
})
Now, even if you reload or restart, your events remain stored in Replit’s Database. You can verify stored data via the Replit “Database” side panel.
/api/events), since Replit serves backend and frontend under the same domain.
You have a working, persistent event calendar web app built on Replit — using Express for backend, a simple frontend served from the same Repl, and persistent storage with Replit DB. You can extend it by adding user authentication, sorting events by date, or migrating to a real calendar UI library later, but the structure here is fully real and deployable within Replit’s environment today.
<!-- server.js -->
<script type="module">
import express from "express";
import bodyParser from "body-parser";
import { v4 as uuidv4 } from "uuid";
const app = express();
app.use(bodyParser.json());
let events = [
{
id: uuidv4(),
title: "Team Sync",
date: "2024-10-01",
startTime: "10:00",
endTime: "11:00",
description: "Weekly check-in",
},
];
app.get("/api/events", (req, res) => {
const { date } = req.query;
const filtered = date
? events.filter((event) => event.date === date)
: events;
res.json(filtered);
});
app.post("/api/events", (req, res) => {
const { title, date, startTime, endTime, description } = req.body;
if (!title || !date) return res.status(400).json({ error: "Missing fields" });
const newEvent = {
id: uuidv4(),
title,
date,
startTime,
endTime,
description,
};
events.push(newEvent);
res.status(201).json(newEvent);
});
app.put("/api/events/:id", (req, res) => {
const { id } = req.params;
const idx = events.findIndex((e) => e.id === id);
if (idx === -1) return res.status(404).json({ error: "Event not found" });
events[idx] = { ...events[idx], ...req.body };
res.json(events[idx]);
});
app.delete("/api/events/:id", (req, res) => {
const { id } = req.params;
events = events.filter((e) => e.id !== id);
res.json({ message: "Event deleted", id });
});
app.listen(3000, () => console.log("Server running on port 3000"));
</script>
<!-- googleCalendarSync.js -->
<script type="module">
import express from "express";
import { google } from "googleapis";
const app = express();
app.use(express.json());
// Use Replit Secrets for these
const CLIENT_ID = process.env.GOOGLE_CLIENT\_ID;
const CLIENT_SECRET = process.env.GOOGLE_CLIENT\_SECRET;
const REDIRECT_URI = process.env.GOOGLE_REDIRECT\_URI; // e.g., https://your-repl-name.username.repl.co/oauth2callback
const REFRESH_TOKEN = process.env.GOOGLE_REFRESH\_TOKEN;
const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT\_URI);
oAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN });
const calendar = google.calendar({ version: "v3", auth: oAuth2Client });
// Sync event from your local event store to Google Calendar
app.post("/api/syncToGoogle", async (req, res) => {
try {
const { title, description, startTime, endTime } = req.body;
const event = {
summary: title,
description,
start: { dateTime: new Date(startTime).toISOString(), timeZone: "UTC" },
end: { dateTime: new Date(endTime).toISOString(), timeZone: "UTC" },
};
const response = await calendar.events.insert({
calendarId: "primary",
resource: event,
});
res.json({ message: "Synced to Google Calendar", eventId: response.data.id });
} catch (error) {
console.error("Sync failed:", error);
res.status(500).json({ error: "Failed to sync event" });
}
});
app.listen(3000, () => console.log("Google Calendar sync server running on port 3000"));
</script>
<!-- remindersWorker.js -->
<script type="module">
import express from "express";
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
import schedule from "node-schedule";
import nodemailer from "nodemailer";
const app = express();
app.use(express.json());
// setup lowdb for persistent storage inside Replit
const adapter = new JSONFile("events.json");
const db = new Low(adapter, { events: [] });
await db.read();
// setup mail transporter (use Replit secrets for credentials)
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.MAIL\_USER,
pass: process.env.MAIL\_PASS,
},
});
function scheduleReminders() {
db.data.events.forEach((event) => {
if (!event.reminderScheduled) {
const reminderDate = new Date(event.startTime);
reminderDate.setMinutes(reminderDate.getMinutes() - 15); // 15 mins before
if (reminderDate > new Date()) {
schedule.scheduleJob(reminderDate, async () => {
await transporter.sendMail({
from: process.env.MAIL\_USER,
to: event.ownerEmail,
subject: `Upcoming Event: ${event.title}`,
text: `Reminder: "${event.title}" starts at ${event.startTime}.`,
});
console.log(`Reminder sent for ${event.title}`);
});
event.reminderScheduled = true;
}
}
});
db.write();
}
app.post("/api/events/reminder", async (req, res) => {
const { id } = req.body;
const event = db.data.events.find((e) => e.id === id);
if (!event) return res.status(404).json({ error: "Event not found" });
if (!event.reminderScheduled) scheduleReminders();
res.json({ message: "Reminder scheduled if not already" });
});
scheduleReminders();
app.listen(3000, () => console.log("Reminders worker 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.
When building an event calendar app in Replit, treat it like a small but complete full-stack project: keep backend, frontend, and configuration clearly separated. Use Replit’s built-in Node.js templates for the server, React (or plain HTML/JS if you prefer) for the user interface, and store environment secrets (like database credentials or API keys) in Replit’s “Secrets” panel — never hard-coded. Persist data with SQLite for local development or a hosted DB like Supabase. Always test in the Replit preview browser and use the Replit Database or external DB when you need persistence. Since Replit gives you one running server per Repl, this app should use one Express server that also serves the frontend and provides JSON APIs for the calendar data.
Create this structure inside your Replit project:
// In Replit shell:
npm init -y
npm install express sqlite3
mkdir public
touch index.js db.js public/index.html public/script.js public/style.css
Add the following code to index.js at the root of the project. This sets up an Express server, serves your static frontend, and exposes REST API endpoints to create and fetch events.
// index.js
const express = require('express')
const path = require('path')
const db = require('./db') // import database functions
const app = express()
app.use(express.json()) // allows reading JSON
app.use(express.static(path.join(__dirname, 'public'))) // serves frontend files
// Get all events
app.get('/api/events', async (req, res) => {
const events = await db.getAllEvents()
res.json(events)
})
// Add new event
app.post('/api/events', async (req, res) => {
const { title, date } = req.body
if (!title || !date) return res.status(400).send('Missing fields')
await db.addEvent(title, date)
res.sendStatus(201)
})
// Start server (Replit provides process.env.PORT)
const PORT = process.env.PORT || 3000
app.listen(PORT, () => console.log(`Server running on port ${PORT}`))
In Replit, using SQLite is the simplest persistent storage. You can switch to Replit DB or Supabase later. This file contains all DB operations.
// db.js
const sqlite3 = require('sqlite3').verbose()
const db = new sqlite3.Database('./events.db')
// Create table if not exists
db.run('CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, date TEXT)')
// Return all events
function getAllEvents() {
return new Promise((resolve, reject) => {
db.all('SELECT * FROM events ORDER BY date', [], (err, rows) => {
if (err) reject(err)
else resolve(rows)
})
})
}
// Add event
function addEvent(title, date) {
return new Promise((resolve, reject) => {
db.run('INSERT INTO events (title, date) VALUES (?, ?)', [title, date], function (err) {
if (err) reject(err)
else resolve()
})
})
}
module.exports = { getAllEvents, addEvent }
This file goes inside the public folder. It displays a list of events and allows adding new ones. Replit automatically serves it when you open the web preview.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Event Calendar</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>My Event Calendar</h1>
<form id="event-form">
<input type="text" id="title" placeholder="Event title" required />
<input type="date" id="date" required />
<button type="submit">Add Event</button>
</form>
<ul id="event-list"></ul>
<script src="script.js"></script>
</body>
</html>
This script fetches data from the backend and updates the UI dynamically. Use the global Replit preview to test that events show up correctly after adding.
// public/script.js
const list = document.getElementById('event-list')
const form = document.getElementById('event-form')
async function loadEvents() {
const res = await fetch('/api/events')
const events = await res.json()
list.innerHTML = ''
events.forEach(ev => {
const li = document.createElement('li')
li.textContent = `${ev.date}: ${ev.title}`
list.appendChild(li)
})
}
form.addEventListener('submit', async (e) => {
e.preventDefault()
const title = document.getElementById('title').value
const date = document.getElementById('date').value
await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, date })
})
form.reset()
loadEvents()
})
loadEvents()
app.use(express.static('public')).
Following this pattern keeps the code tidy, predictable, and portable — exactly how professional projects grow safely in Replit’s environment.
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.