How to Structure a Node.js Project
SkillVeris Team
Engineering Team

A well-structured Node.js project separates concerns into folders like routes, controllers, services, and models so each file has one clear job.
In this guide, you'll learn:
- Routes map URLs to controllers, controllers handle the request and response, and services hold the business logic.
- Keeping business logic out of route handlers makes it reusable and far easier to test in isolation.
- A config folder centralises environment settings, and a middleware folder holds cross-cutting concerns.
- Start simple and introduce structure as the project grows rather than over-engineering on day one.
1Why Project Structure Matters
A good Node.js project structure organises code into folders by responsibility — routes, controllers, services, models, and config — so that any given piece of logic has one obvious home. When the layout is predictable, you and your teammates can find and change code without hunting through a tangle of files.
Node itself is unopinionated about structure, which is freeing but also a trap: without discipline, everything drifts into a giant index.js. A deliberate layout keeps the codebase navigable as it grows from a weekend prototype into something a team maintains for years.
2The Layered Approach
The most durable pattern separates the app into layers, each with a single responsibility. A request flows through them in order, and each layer only knows about the one directly beneath it.
- Routes: define URLs and HTTP methods, delegate to controllers.
- Controllers: read the request, call services, shape the response.
- Services: contain business logic, independent of HTTP.
- Models: define data shapes and talk to the database.
- Middleware: cross-cutting concerns like auth and logging.
🔑One Job Per Layer
The value of layering is that each file does one thing. A controller never runs a database query directly, and a service never touches req or res.
3A Sample Folder Layout
A typical layout puts source code under src/ with a folder per layer. The entry file wires everything together, while the real work lives in focused modules.
- src/routes/ — route definitions
- src/controllers/ — request handlers
- src/services/ — business logic
- src/models/ — data models
- src/middleware/ — auth, logging, errors
- src/config/ — env and settings
- src/app.js — wires up the app
- server.js — starts the server
4Separating Routes, Controllers, and Services
The clearest win comes from splitting the three request-facing layers. The route just points a URL at a controller. The controller handles the HTTP details. The service does the actual work and knows nothing about HTTP, which is what makes it reusable and testable.
- // routes/users.js
- router.get('/:id', userController.getUser)
- // controllers/userController.js
- async function getUser(req, res) {
- const user = await userService.findById(req.params.id)
- user ? res.json(user) : res.status(404).end()
- }
- // services/userService.js — pure logic, no req/res
Why Services Are Testable
Because a service takes plain arguments and returns plain data, you can unit-test it without spinning up an HTTP server or faking request objects. That single boundary makes the majority of your logic trivial to test.
5Configuration and Environment
Centralise configuration in one place so settings are not scattered across files. A config module reads environment variables once, applies defaults, and exports a clean object the rest of the app imports — meaning process.env appears in exactly one file.
- // config/index.js
- module.exports = {
- port: process.env.PORT || 3000,
- dbUrl: process.env.DATABASE_URL,
- jwtSecret: process.env.JWT_SECRET
- }
💡One Place for Config
Reading environment variables in a single config module means you can validate them in one spot and swap defaults without touching business code.
6Start Simple, Grow Deliberately
Do not impose a ten-folder architecture on a hundred-line script. Structure is a response to complexity, not a prerequisite for it. A small app can live happily in a few files, and you introduce layers as the pain of not having them appears.
The signal to add structure is repetition and confusion: when you find the same logic copied into two routes, or you cannot remember where a piece of code lives, it is time to extract a service or a module. Let the codebase tell you what it needs.
7Best Practices
A few conventions keep a growing project coherent.
- Group by responsibility (routes, services) rather than by feature until the app is large.
- Keep controllers thin — they should orchestrate, not contain business logic.
- Name files and folders consistently so the layout is self-explanatory.
- Export a single responsibility per module to keep imports predictable.
- Put the server bootstrap in its own file, separate from the app definition, so tests can import the app without starting a listener.
8Key Takeaways
Structure is about making code easy to find, change, and test.
- Separate routes, controllers, services, and models by responsibility.
- Keep business logic in services, isolated from HTTP concerns.
- Centralise configuration so process.env lives in one module.
- Split the app definition from the server bootstrap for testability.
- Start simple and add layers as complexity actually demands them.
9Frequently Asked Questions
Q: What is the difference between a controller and a service? A: A controller deals with HTTP — it reads the request, calls the appropriate logic, and sends a response. A service contains the business logic and knows nothing about req or res, which makes it reusable and easy to unit-test.
Q: Should I structure my project by layer or by feature? A: Small and medium apps are usually clearest grouped by layer (routes, controllers, services). Very large codebases often benefit from grouping by feature so each domain is self-contained. Start with layers and refactor toward features if the app grows.
Q: Why separate app.js from server.js? A: Keeping the Express app definition separate from the code that starts listening lets your tests import the app and make requests against it without opening a real port, which makes integration tests faster and simpler.
Q: Is there one correct Node.js project structure? A: No. Node is unopinionated, so conventions vary. The important thing is consistency and separation of concerns — pick a clear layout, apply it uniformly, and let the project's growth guide when to add more structure.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.