Skip to main content
Node.js Routes vs Controllers vs Services
0
Technology

Node.js Routes vs Controllers vs Services

Learn the difference between routes, controllers, and services in Node.js. Understand their roles, folder structure, and best practices with simple examples.

Published Jun 22, 2026 Updated Jun 23, 2026 5 min read3 views
Read in:

Node.js Routes vs Controllers vs Services: A Simple Guide for Clean Project Structure

When building a Node.js application using Express, many developers put all their code inside route files. This works for small projects, but as the application grows, the code becomes difficult to manage.

A better approach is to separate your application into three layers:

  • Routes
  • Controllers
  • Services

This structure makes the code easier to read, test, maintain, and scale.

In this article, you'll learn what each layer does, why it is important, and how they work together.

Why Separate Code into Layers?

Without separation:

router.get("/users/:id", async (req, res) => {
const user = await
User.findById(req.params.id);
if (!user) { 
   return
res.status(404).json({
   message: "User not found",
    });
  }
  res.json(user);
});

This route is handling:

  • Request handling
  • Business logic
  • Database operations
  • Response formatting

As the application grows, route files become large and difficult to manage.

Instead, separate responsibilities:

Request

   ↓

Route

   ↓

Controller

   ↓

Service

   ↓

Database

Each layer has one clear job.

What Are Routes?

Routes define application endpoints.

They decide which controller should run when a specific URL is requested.

Responsibilities of Routes

  • Define API URLs    
  • Define HTTP methods
  • Connect requests to controllers
  • Should not contain business logic
  • Should not contain database queries

Example Route

const express = require("express");
const router = express.Router();
const userController = require("../controllers/userController");
router.get("/:id", userController.getUser);
module.exports = router;

Here the route only maps:

GET /users/:id

to:

userController.getUser

Nothing more.

What Are Controllers?

Controllers receive requests from routes.

They handle:

  • Request data
  • Validation
  • Calling services
  • Sending responses

Controllers act as a bridge between routes and services.

Responsibilities of Controllers

  • Read request parameters
  • Read request body
  • Call services
  • Return API responses
  • Should not contain complex business logic
  • Should not directly access the database

Example Controller

const userService = require("../services/userService");
const getUser = async (req, res) => {
  try {
    const userId = req.params.id;
    const user = await userService.getUserById(userId);
    return res.status(200).json(user);
  } catch (error) {
    return res.status(500).json({
      message: error.message,
    });
  }
};
module.exports = { getUser,};

The controller:

  1. Gets the user ID
  2. Calls the service
  3. Sends the response

What Are Services?

Services contain the actual business logic. This is where application rules are implemented.

Examples:

  • User registration
  • Login
  • Payment processing
  • Order creation
  • Email sending
  • Data calculations

Responsibilities of Services

  • Business logic
  • Database queries
  • External API calls
  • Data processing
  • Should not send HTTP responses
  • Should not know about Express request or response objects

Example Service

const User = require("../models/User");
const getUserById = async (id) => {
const user = await User.findById(id);
  if (!user) {
throw new Error("User not found");
  }
return user;
};
module.exports = { getUserById,};

The service focuses only on user-related logic.

It does not know anything about Express.

Complete Flow Example

Let's see how all three layers work together.

Route

router.get("/:id", userController.getUser);

Controller

const getUser = async (req, res) => {
const user = await userService.getUserById(
req.params.id
  );
res.json(user);
};

Service

const getUserById = async (id) => {
return await User.findById(id);
};

Request Flow

Client Request

      ↓

Route

      ↓

Controller

      ↓

Service

      ↓

Database

      ↓

Service

      ↓

Controller

      ↓

Response

Recommended Folder Structure

A common project structure looks like this:

project/

├── routes/

│   └── userRoutes.js

├── controllers/

│   └── userController.js

├── services/

│   └── userService.js

├── models/

│   └── User.js

├── middlewares/

├── config/

├── utils/

└── app.js

This structure keeps files organized and easy to find.

Example: User Registration

Route

router.post("/register",userController.register);

Controller

const register = async (req, res) => {
  try {
    const user = await userService.registerUser(
      req.body
    );
return res.status(201).json(user);
} catch (error) {
return res.status(400).json({
      message: error.message,
    });
  }
};

Service

const bcrypt = require("bcrypt");
const User = require("../models/User");
const registerUser = async (data) => {
  const existingUser =
    await User.findOne({ email: data.email,
    });
  if (existingUser) {
    throw new Error("Email already exists");
  }
  const hashedPassword = await
  bcrypt.hash(data.password, 10);
  const user = await User.create({...data,password: hashedPassword,
  });
  return user;
};
module.exports = { registerUser,
};

Notice that password hashing and email checks belong in the service because they are business rules.

Benefits of Using Routes, Controllers, and Services

1. Cleaner Code

Each file has a single responsibility.

  • Routes handle routes.
  • Controllers handle requests.
  • Services handle logic.

2. Easier Maintenance

When a bug occurs, you know exactly where to look.

  • Route issue: routes
  • Response issue: controllers
  • Business logic issue: services

3. Better Reusability

Services can be reused in multiple controllers.

Example:

userService.getUserById(id);

can be used in:

  • Profile APIs
  • Admin APIs
  • Order APIs

4. Easier Testing

Services can be tested independently.

Example:

await userService.registerUser(data);

No Express server is required.

5. Better Team Collaboration

Different developers can work on different layers without conflicts.

  • Backend developer: Services
  • API developer: Controllers
  • Integration developer: Routes

Common Mistakes

Putting DatabaseQueries in Routes

Bad:

router.get("/:id", async (req, res) => {
const user = await User.findByIreq.params.id
  );
});

Good:

router.get(
  "/:id", userController.getUser
);

Writing Business Logic in Controllers

Bad:

const hashedPassword = await bcrypt.hash(password, 10);

Controllers should not handle business rules.

Move it to services.

Using Response Objects Inside Services

Bad:

res.status(400).json({
 message: "User not found",
});

Services should return data or throw errors.

Controllers should handle responses.

When Can You Skip Services?

For very small projects:

Route: Controller may be enough.

However, once the application starts growing, adding a service layer is highly recommended.

Most production applications follow:

Route

  ↓

Controller

  ↓

Service

  ↓

Database

because it scales much better.

Best Practices

1. Keep Routes Thin

router.get( "/users/:id", userController.getUser);

2. Keep Controllers Simple

const user = await userService.getUserById(id);

3. Keep Business Logic in Services

const registerUser = async (data) => {
  // business rules
};

4. Keep Database Access Organized

Use services or repositories for database operations instead of writing queries everywhere.

Final Thoughts

Routes, controllers, and services each have a specific purpose in a Node.js application.


Layer


Responsibility


Routes


Define API endpoints and connect requests to controllers


Controllers


Handle requests and responses

Services


Contain business logic and database operations

A simple rule to remember:

  • Routes decide where the request goes.
  • Controllers manage the request and response.
  • Services perform the actual work.

📂 Categories

🏷️ Tags

About the author Vivek Verma

Software Developer & Co-Founder of ApnaInsights. Writes on technology, software development, and practical career guidance for Indian professionals.

0Blogs
0Followers

Discussion

AnonymousGuest