MERN Stack

Building a Simple Event Management App with MERN Stack

By the Domain India teamPublished 10 min read
Knowledge base article
Contents (11 sections)

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.

Key takeaways

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

ToolCheck withNotes
Node.js (current LTS)node -vNode.js 24 is the Active LTS release in September 2026; install it from nodejs.org or with nvm
npmnpm -vComes with Node.js
MongoDBmongoshA free MongoDB Atlas cluster is simplest; MongoDB Community on your computer also works
A code editorVS 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.

bash
mkdir event-manager && cd event-manager
npm init -y
npm install express mongoose
mkdir models routes

Edit package.json so it has these scripts:

json
{
  "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:

text
MONGODB_URI=mongodb+srv://USER:[email protected]/events
PORT=5000

Node.js reads .env itself with --env-file, so you don't need the dotenv package.

3. The data model

Create models/Event.js:

javascript
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:

javascript
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:

javascript
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:

bash
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/events

The 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:

bash
npm create vite@latest client -- --template react
cd client
npm install

Point API calls at the Express server during development by editing client/vite.config.js:

javascript
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:

jsx
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 client writes static files to client/dist, which server.js serves.
  • 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.

  1. Create the app
    in Services › App Platform with New App.
  2. Add your connection string
    in the app's Configure › Env Vars tab as MONGODB_URI. Don't set PORT; the platform sets it.
  3. 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.
  4. Create a deploy token
    in Access › Deploy Tokens and copy the ready-to-run command it shows.
  5. Build and package
    from the folder that contains event-manager, then run the copied command.
bash
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:

  1. Install a Node.js LTS release and MongoDB Community from MongoDB's official packages for your Linux distribution.
  2. Enable MongoDB authentication, keep it listening on 127.0.0.1 only, and set MONGODB_URI for your app.
  3. Run the app with PM2 or a systemd service so it restarts after crashes and reboots.
  4. Put nginx in front as a reverse proxy to localhost:5000, and get a free Let's Encrypt certificate with certbot --nginx. Let nginx handle HTTPS rather than your Node.js code.
  5. 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 wantChoose
Deploys without managing a server, with SSL and your own domain handled for youApp Platform, with MongoDB Atlas or the included PostgreSQL
MongoDB on your own server, full root access, several appsVPS

Prices on the cards are Domain India list prices, excluding 18% GST.

App Starter
₹100/mo + GST
  • 512 MB RAM per app
  • 1 vCPU
  • 5 GB NVMe SSD
  • PostgreSQL Database
See plan details
VPS Starter
₹552.65/mo + GST
  • 1 vCPU
  • 2 GB DDR4 RAM
  • 64 GB NVMe SSD Storage
  • 2 TB Monthly Bandwidth
See plan details

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.

Deploy your MERN app

Node.js apps are detected automatically, with free SSL, custom domains and deploy tokens on every plan.

See App Platform plans

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Build an Event Management App with MERN (2026 Guide)