Server Setup
Splitting the Express app into app.js and server.js — the proper structure for a production-ready backend.
Server Setup
In the previous file we used a single server.js just to verify the setup worked. Now we split it properly — app.js holds the Express application and all its configuration, server.js is a thin entry point that connects to the database before starting the server.
This separation matters for testing — you can import app.js into a test file without actually starting a server.
Why Split app.js and server.js
app.js — defines the Express app, middleware, and routes. No listen() call here.
server.js — connects to MongoDB first, and only starts the server if the connection succeeds.
This way the server never starts accepting requests before the database is ready.
app.js — Express Application
// src/app.js
import express from "express";
import cors from "cors";
const app = express();
// Step 1 — built-in middleware to parse JSON request bodies
app.use(express.json());
// Step 2 — parse URL-encoded form data
app.use(express.urlencoded({ extended: true }));
// Step 3 — enable CORS so the frontend can make requests to this API
app.use(
cors({
origin: process.env.CLIENT_URL || "http://localhost:5173",
credentials: true, // allow cookies to be sent cross-origin
})
);
// Step 4 — health check route — useful for deployment monitoring
app.get("/api/health", (req, res) => {
res.status(200).json({ status: "ok", message: "Server is healthy" });
});
// Step 5 — routes will be mounted here as we build them
// Example (added in later files):
// import authRoutes from "./routes/auth.routes.js";
// app.use("/api/auth", authRoutes);
// Step 6 — 404 handler — runs if no route matched
app.use((req, res) => {
res.status(404).json({ message: "Route not found" });
});
export default app;server.js — Entry Point
// server.js
import dotenv from "dotenv";
dotenv.config(); // load .env variables before anything else uses them
import app from "./src/app.js";
import connectDB from "./src/config/db.js";
const PORT = process.env.PORT || 5000;
// Step 1 — connect to MongoDB first
connectDB()
.then(() => {
// Step 2 — only start listening once the database connection succeeds
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
})
.catch((error) => {
// Step 3 — if connection fails, log the error and exit the process
console.error("Failed to connect to database:", error.message);
process.exit(1);
});dotenv.config() must run before importing any file that reads process.env values — like db.js, which reads MONGO_URI. Importing dotenv and calling config() at the very top of server.js guarantees this order.
Placeholder — db.js
We build the real db.js in the next file. For now, a placeholder so server.js runs without errors:
// src/config/db.js
// Full implementation in the next file — mongodb-connection.mdx
const connectDB = async () => {
console.log("Database connection placeholder — see next file");
};
export default connectDB;Run It
npm run dev# expected output
Database connection placeholder — see next file
Server running on port 5000Visit http://localhost:5000/api/health — you should see:
{ "status": "ok", "message": "Server is healthy" }Summary
app.jsholds the Express app, middleware, and routes — never callslisten()server.jsis the entry point — loads env variables, connects to the database, then starts the server- This split makes the app testable and ensures the server never starts before the database is ready
corsis configured withcredentials: trueto support cookie-based auth later- A
/api/healthroute is useful for uptime monitoring once deployed - Next file — MongoDB Connection — builds out the real
db.js