A common Express convention is splitting app.js (the application logic) from server.js
(the process that starts listening). It's a small change with a real payoff in testability
and clarity.
What app.js is responsible for
app.js builds the Express application: middleware, routes, settings — everything except
actually starting a server.
// app.js
const express = require("express");
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
res.send("Bookstore API");
});
module.exports = app;
- Middleware like
express.json()is registered here. - Routes are wired up here.
- The app is exported, not started, so other files (tests,
server.js) can use it.
What server.js is responsible for
server.js imports the app, decides the port, and starts listening.
// server.js
const app = require("./app");
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server is running at http://localhost:${PORT}`);
});
- Imports the configured app.
- Reads the port from the environment (falling back to a default).
- Starts the server and logs a status message.
Why bother splitting them
- Separation of concerns —
app.jsis "what the app does",server.jsis "how it runs". Each file has one job. - Cleaner organization — adding a route or middleware means editing
app.jsonly; you never touch server startup code to do it. - Easier debugging — a routing bug and a "server won't start" bug live in different places.
- Scales with the project — new features slot into
app.jswithout risk to the listening logic. - Testability — since
app.jsexports the Express instance without callinglisten(), test frameworks (like Supertest) can import it directly, without spinning up a real server or port.