This tutorial builds a small event management app with the MERN stack: MongoDB for data, Express and Node.js for the API, and React for the interface. You'll create an API that lists, adds, updates and deletes events, a React page that uses it, and then deploy the finished app, either on the Domain India App Platform or on a VPS.
Use a current Node.js LTS release, Express 5, Mongoose and a React app created with Vite (Create React App is deprecated). Keep the API and the built React files in one Node.js project, read the port from PORT, bind to 0.0.0.0 and keep the MongoDB connection string in an environment variable. To go live, deploy to the App Platform with a deploy token and a hosted MongoDB such as MongoDB Atlas (the App Platform includes PostgreSQL, not MongoDB), or run everything yourself on a VPS.
1. What you need
| Tool | Check with | Notes |
|---|---|---|
| Node.js (current LTS) | node -v | Node.js 24 is the Active LTS release in September 2026; install it from nodejs.org or with nvm |
| npm | npm -v | Comes with Node.js |
| MongoDB | mongosh | A free MongoDB Atlas cluster is simplest; MongoDB Community on your computer also works |
| A code editor | VS Code or any editor you like |
You should be comfortable with basic JavaScript and the command line. For a refresher, see Installing Node.js and npm on your local machine.
2. Create the project
The API lives in the project root and the React app in a client folder. Deploying them as one app is simpler than running two.
mkdir event-manager && cd event-manager
npm init -y
npm install express mongoose
mkdir models routesEdit package.json so it has these scripts:
{
"scripts": {
"start": "node server.js",
"dev": "node --watch --env-file=.env server.js"
}
}Create a .env file for local development only, and add .env and node_modules to .gitignore:
MONGODB_URI=mongodb+srv://USER:[email protected]/events
PORT=5000Node.js reads .env itself with --env-file, so you don't need the dotenv package.
3. The data model
Create models/Event.js:
const { Schema, model } = require('mongoose');
const eventSchema = new Schema(
{
title: { type: String, required: true, trim: true, maxlength: 120 },
description: { type: String, trim: true, maxlength: 2000 },
date: { type: Date, required: true },
location: { type: String, required: true, trim: true },
},
{ timestamps: true }
);
module.exports = model('Event', eventSchema);4. The API routes
Create routes/events.js with all five operations:
const express = require('express');
const Event = require('../models/Event');
const router = express.Router();
const pick = ({ title, description, date, location }) => ({ title, description, date, location });
router.get('/', async (req, res) => {
res.json(await Event.find().sort({ date: 1 }));
});
router.get('/:id', async (req, res) => {
const event = await Event.findById(req.params.id);
if (!event) return res.status(404).json({ error: 'Event not found' });
res.json(event);
});
router.post('/', async (req, res) => {
const event = await Event.create(pick(req.body));
res.status(201).json(event);
});
router.put('/:id', async (req, res) => {
const event = await Event.findByIdAndUpdate(req.params.id, pick(req.body), {
new: true,
runValidators: true,
});
if (!event) return res.status(404).json({ error: 'Event not found' });
res.json(event);
});
router.delete('/:id', async (req, res) => {
const event = await Event.findByIdAndDelete(req.params.id);
if (!event) return res.status(404).json({ error: 'Event not found' });
res.status(204).end();
});
module.exports = router;pick copies only the fields you expect, so a visitor can't write extra fields into your database. Express 5 passes errors from async handlers to your error handler automatically, so you don't need a try/catch in every route.
5. The server
Create server.js:
const path = require('path');
const express = require('express');
const mongoose = require('mongoose');
const eventRoutes = require('./routes/events');
const app = express();
app.use(express.json());
app.use('/api/events', eventRoutes);
// Serve the built React app
const clientDist = path.join(__dirname, 'client', 'dist');
app.use(express.static(clientDist));
app.use((req, res, next) => {
if (req.method !== 'GET' || req.path.startsWith('/api')) return next();
res.sendFile(path.join(clientDist, 'index.html'));
});
// Bad input gives 400; anything else gives 500 without leaking details
app.use((err, req, res, next) => {
const badInput = err.name === 'ValidationError' || err.name === 'CastError';
if (!badInput) console.error(err);
res.status(badInput ? 400 : 500).json({ error: badInput ? err.message : 'Server error' });
});
const port = process.env.PORT || 5000;
mongoose
.connect(process.env.MONGODB_URI)
.then(() => app.listen(port, '0.0.0.0', () => console.log(`Listening on port ${port}`)))
.catch((err) => {
console.error('MongoDB connection failed:', err.message);
process.exit(1);
});Two details matter when you deploy: the app reads its port from PORT, and it binds to 0.0.0.0 so it can be reached from outside its container.
Start it with npm run dev and test from a second terminal:
curl -X POST http://localhost:5000/api/events \
-H "Content-Type: application/json" \
-d '{"title":"Annual meetup","date":"2026-12-05","location":"Community Hall"}'
curl http://localhost:5000/api/eventsThe first command returns the new event with an _id and status 201. A request without a title returns 400 with the validation message.
6. The React frontend
Create the React app with Vite, inside the project:
npm create vite@latest client -- --template react
cd client
npm installPoint API calls at the Express server during development by editing client/vite.config.js:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: { proxy: { '/api': 'http://localhost:5000' } },
});Replace client/src/App.jsx with a list and a form:
import { useEffect, useState } from 'react';
const empty = { title: '', description: '', date: '', location: '' };
export default function App() {
const [events, setEvents] = useState([]);
const [form, setForm] = useState(empty);
const [error, setError] = useState('');
async function load() {
const res = await fetch('/api/events');
setEvents(await res.json());
}
useEffect(() => { load(); }, []);
async function handleSubmit(e) {
e.preventDefault();
setError('');
const res = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form),
});
if (!res.ok) return setError((await res.json()).error);
setForm(empty);
load();
}
async function remove(id) {
await fetch(`/api/events/${id}`, { method: 'DELETE' });
load();
}
const field = (name) => (e) => setForm({ ...form, [name]: e.target.value });
return (
<main>
<h1>Events</h1>
<form onSubmit={handleSubmit}>
<input placeholder="Title" value={form.title} onChange={field('title')} required />
<input type="date" value={form.date} onChange={field('date')} required />
<input placeholder="Location" value={form.location} onChange={field('location')} required />
<textarea placeholder="Description" value={form.description} onChange={field('description')} />
<button type="submit">Add event</button>
</form>
{error && <p role="alert">{error}</p>}
<ul>
{events.map((ev) => (
<li key={ev._id}>
<strong>{ev.title}</strong>, {new Date(ev.date).toLocaleDateString('en-IN')}, {ev.location}
<button onClick={() => remove(ev._id)}>Delete</button>
</li>
))}
</ul>
</main>
);
}Run npm run dev in client while the API runs in the root, and open the address Vite prints (usually http://localhost:5173). Style it with plain CSS, or add a UI library such as Bootstrap or MUI.
7. Before you go live
- Build the frontend:
npm run build --prefix clientwrites static files toclient/dist, whichserver.jsserves. - Keep secrets out of code: the MongoDB connection string belongs in an environment variable, never in Git.
- Add authentication before real users can edit or delete events; right now anyone can.
- Validate on the server. The Mongoose schema rejects missing fields, but add limits for anything you accept from visitors.
8. Deploy on the App Platform
The App Platform detects a Node.js app from package.json and runs your start script. It includes a PostgreSQL database, not MongoDB, so keep your data on a hosted MongoDB such as MongoDB Atlas.
- Create the appin Services › App Platform with New App.
- Add your connection stringin the app's Configure › Env Vars tab as
MONGODB_URI. Don't setPORT; the platform sets it. - Allow the connection in Atlas.Atlas accepts connections only from addresses on its Network Access list; ask support which outbound address to allow for your app.
- Create a deploy tokenin Access › Deploy Tokens and copy the ready-to-run command it shows.
- Build and packagefrom the folder that contains
event-manager, then run the copied command.
npm run build --prefix event-manager/client
tar czf app.tar.gz --exclude=node_modules --exclude=.git --exclude=.env -C event-manager .Follow the build in the Deploys tab, then open your app's address from Overview. Pushing to GitHub doesn't deploy by itself: use the Deploy Now button, or a deploy-token step in your CI. See Getting started with the App Platform and Deploying with a deploy token.
If you'd rather use the included database, the same app works with PostgreSQL and a library such as Prisma in place of Mongoose. Your app receives the address in DATABASE_URL; see PostgreSQL database.
9. Deploy on a VPS
A VPS gives you root access, so you can run MongoDB on the same server. The usual setup:
- Install a Node.js LTS release and MongoDB Community from MongoDB's official packages for your Linux distribution.
- Enable MongoDB authentication, keep it listening on
127.0.0.1only, and setMONGODB_URIfor your app. - Run the app with PM2 or a systemd service so it restarts after crashes and reboots.
- Put nginx in front as a reverse proxy to
localhost:5000, and get a free Let's Encrypt certificate withcertbot --nginx. Let nginx handle HTTPS rather than your Node.js code. - Open only ports 22, 80 and 443 in the firewall.
See Installing MongoDB on a Domain India VPS and PM2 process management.
10. Which Domain India option fits
| You want | Choose |
|---|---|
| Deploys without managing a server, with SSL and your own domain handled for you | App Platform, with MongoDB Atlas or the included PostgreSQL |
| MongoDB on your own server, full root access, several apps | VPS |
Prices on the cards are Domain India list prices, excluding 18% GST.
- 512 MB RAM per app
- 1 vCPU
- 5 GB NVMe SSD
- PostgreSQL Database
- 1 vCPU
- 2 GB DDR4 RAM
- 64 GB NVMe SSD Storage
- 2 TB Monthly Bandwidth
Frequently asked questions
What is the MERN stack?
MERN is MongoDB, Express, React and Node.js. MongoDB stores the data, Express and Node.js run the API on the server, and React builds the interface in the browser, all in JavaScript.
Should I use Create React App for a new MERN project?
No. Create React App is deprecated. Create the React app with Vite (npm create vite@latest) or a framework such as Next.js.
Which Node.js version should I use?
Use the current Active LTS release. In September 2026 that is Node.js 24. Avoid odd-numbered releases for production.
Does the Domain India App Platform include MongoDB?
No. Every App Platform plan includes a PostgreSQL database. For MongoDB, use a hosted service such as MongoDB Atlas and put its connection string in your own environment variable, or run MongoDB yourself on a VPS.
Why does my app build on the App Platform but not load?
Usually the app isn't listening on the PORT environment variable or is bound to localhost. Listen on PORT and bind to 0.0.0.0, then deploy again.
How do I serve the React app and the API together?
Build React into client/dist with npm run build, serve that folder with express.static, and send index.html for any GET request that isn't under /api.
Do I need the dotenv package?
Not for local development on current Node.js releases. Run node --env-file=.env server.js. In production, set environment variables on the platform instead of shipping a .env file.
Ready to launch your app? Compare App Platform plans and VPS plans, open your App Platform apps, or ask us in a support ticket.
Node.js apps are detected automatically, with free SSL, custom domains and deploy tokens on every plan.
See App Platform plans