Application Structure

By Syncfusion®, Inc. · Last updated

Splitting app.js and server.js, a project folder layout, a central config module, registration order, startup errors and graceful shutdown, testing with node:test and Supertest, Express Generator, and keeping code healthy.

What you will learn

  • Split app.js and server.js
  • Organise routes, handlers, middleware and config
  • Register middleware in the right order
  • Handle startup errors and SIGTERM
  • Test routes with node:test and Supertest

Application Structure Introduction

Small demos fit in one file. Real apps do not.

  • Application structure = how you split an app into files and folders.
  • Good structure makes code easy to find, change and test.
  • Express does not force a layout. You choose one and keep it consistent.
  • This lesson shows a common, simple layout for the canteen API.
  • It also shows the order to register everything, and how to test it.

Simple lesson: Put each kind of code in its own place, and register it in a fixed order.

A Real-World Example

Think about a well-run school kitchen.

  1. Vegetables, spices and plates each have their own shelf.
  2. Every cook knows where to find things, even on their first day.
  3. The work happens in a fixed order: wash, cut, cook, serve.
  4. A food inspector can check each station on its own.

Shelves are folders. The fixed order is the registration order. The inspector is your tests.

In this lesson, you will learn:

  1. Why to split app.js and server.js.
  2. A folder layout for a real Express project.
  3. A central configuration module.
  4. The order to register settings, middleware, routes and errors.
  5. How to start and stop the server safely.
  6. How to test routes with Supertest.

Project Layout

canteen-api/
├── src/
│   ├── app.js             builds and exports the Express app
│   ├── server.js          starts the HTTP server
│   ├── config/index.js    reads and checks settings
│   ├── routes/            one router per resource
│   ├── handlers/          the functions routes call
│   ├── middleware/        logger, auth, validation, errors
│   ├── utils/             small shared helpers
│   └── views/             templates, if the app renders pages
├── public/                static files
├── test/                  automated tests
├── .env  .env.example  .gitignore
└── package.json
  • src/ = all the code that runs, kept apart from settings and tests.
  • routes/ = which URL goes where. handlers/ = what happens there.
  • middleware/ = shared steps, such as checks and logging.
  • config/ = the only place that reads process.env.
  • utils/ = small helpers, such as formatPrice.
  • public/ and views/ = files for browsers. See Views and Static Files.

Simple lesson: Routes, handlers, middleware and config each get their own folder.

app.js and server.js

  • app.js creates the app, registers everything and exports it. It never calls listen.
  • server.js imports the app, reads the port and calls listen.
  • Tests import app.js and call it directly, with no real port.
  • You can start the same app in different ways, such as with HTTPS or in a cloud function.
  • This split was introduced in Introduction to Express.js. Here it carries a whole project.
  • Open the explorer to see the whole tree.
  • Each file is short, and its job is clear from its folder and name.

Simple lesson: app.js wires everything up. server.js only starts it.

Central Configuration

  • One config module reads process.env and checks it.
  • Every other file imports config, never process.env directly.
  • Convert types once: Number(process.env.PORT ?? 3000).
  • Fail at startup when a required value is missing.
  • Changing a setting name later means changing one file.

Simple lesson: One file reads the environment. Everyone else reads config.

Registration Order

The order of app.use and app.set calls decides how every request flows.

  1. Settings: trust proxy, x-powered-by, view engine.
  2. Security: helmet(), cors(), rate limits.
  3. Logging: request logger or Morgan.
  4. Parsers: express.json(), express.urlencoded(), then cookies and sessions.
  5. Static files: express.static().
  6. Authentication: middleware that sets req.user.
  7. Routers: your routes.
  8. Not found: the 404 handler.
  9. Errors: the four-input error handler, always last.

Easy way to remember: settings, shields, logs, parsers, files, users, routes, 404, errors.

  • Compare Headers: /menu has no security headers, /later has them.
  • Move app.use(helmet()) to the top, and both routes are protected.

Order bugs give no error message. The app works, just less safely. Review the registration order in every code review.

Simple lesson: Middleware only helps what comes after it, so order is everything.

Starting and Stopping the Server

  • Read the port from config.
  • Handle startup errors: in Express 5, the listen callback gets the error, such as a busy port.
  • Log one clear line when the server is ready.
  • Graceful shutdown = finish current requests before stopping.
  • Hosts send SIGTERM before stopping a server. Listen for it, call server.close(), then exit.
const server = app.listen(config.port, () => console.log("Ready"));

process.on("SIGTERM", () => {
  console.log("SIGTERM received, closing the server");
  server.close(() => {
    // Close database connections here.
    process.exit(0);
  });
});

In this lab, the Stop button ends the server at once, like Ctrl + C. On a real host, the SIGTERM code above lets requests in progress finish first.

  • Instead of a long stack trace, the terminal shows one helpful line.
  • process.exit(1) tells the host the start failed, so it can alert you or retry.

Simple lesson: Start with clear errors, and stop gently so no request is cut off.

Testing Routes with Supertest

Automated test = code that checks your code, run with one command.

  • node:test = Node.js's built-in test runner. Run it with node --test.
  • node:assert/strict = checks such as assert.equal(a, b).
  • Supertest = sends requests straight to your app, with no real port. Install: npm install --save-dev supertest.
  • await request(app).get("/api/menu").expect(200) checks one route.
  • Test files usually end in .test.js and live in test/.
  • Press ▶ Run. The terminal runs node --test and shows each test.
  • Change "Idli" in the test to "Dosa" and run again. One test fails and says why.
  • Tests import app.js, which is why it must not call listen.
  • The test fails with expected 201 "Created", got 200 "OK".
  • Change status(200) to status(201) in app.js, and the test passes.

Simple lesson: Export the app, test it with Supertest, and run node --test before every change.

Express Generator

express-generator = an official tool that creates a starter project.

npx express-generator --view=ejs canteen-site
  • It creates app.js, bin/www (the server), routes/, views/ and public/.
  • --view picks the template engine, such as ejs or pug.
  • It uses older CommonJS style (require), not import.
  • It is quick for server-rendered sites, but you will change most files.
  • Building the structure by hand, as in this lesson, teaches you every part.

Simple lesson: The generator gives a fast start. A hand-made layout gives you full control.

Keeping the Code Healthy

  • Small route files: if a file passes about 200 lines, split it.
  • Reuse handlers and middleware instead of copying code.
  • Consistent names: routes/menu.js, handlers/menu.js, middleware/auth.js.
  • One-way imports: routes import handlers, handlers import utils. Never the other way.
  • Circular imports = two files that import each other. Values may be undefined at startup. Move shared code to a third file.

Simple lesson: Small files, clear names and one-way imports keep a project easy to grow.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
app.listen inside app.jsOne file is simplerapp.js exports, server.js listensTests start real servers and clash
process.env in every fileIt is always availableRead it once in configTypos and missing checks everywhere
404 handler before routesOrder does not matterRoutes, then 404, then errorsEvery request gets 404
No SIGTERM handlingStopping is instant anywayClose the server gentlyUsers see cut-off requests during deploys
Business logic in route filesRoutes can do everythingRoutes point, handlers workHuge files that are hard to test
Two modules importing each otherImports always workAvoid cyclesundefined values at startup
No testsManual checks are enoughAutomate with node --testOld features break silently

Check Yourself

Exercises

What to Remember

  • app.js builds and exports the app. server.js starts it.
  • Folders: routes, handlers, middleware, config, utils, views, public, test.
  • One config module reads and checks process.env.
  • Register in order: settings, security, logging, parsers, static, auth, routes, 404, errors.
  • Handle startup errors, and shut down gently on SIGTERM.
  • Test routes with Supertest and node --test.

Next: Express with TypeScript. You will add types to your Express app and catch mistakes early.