Organizing Your Express API with Routes: Why server.js Shouldn't Do Everything
As a backend grows, keeping every endpoint inside server.js quickly becomes difficult to manage. Learn how Express Router and a dedicated routes folder help organize API endpoints without adding unnecessary complexity.

Organizing Your Express API with Routes: Why server.js Shouldn't Do Everything
In the previous Backend Development update, we reached an important milestone.
We turned our basic Node.js project into a real Express server.
Our backend can now:
- Start an HTTP server
- Listen on a port
- Receive requests
- Match a route
- Send JSON responses
Our application currently looks roughly like this:
textcommunity-platform-api/│├── node_modules/│├── src/│ └── server.js│├── .gitignoreAnd inside server.js, we have something similar to:
jsimport express from "express";const app = express();const PORT = 3000;app.get("/", (req, res) => { res.json({For such a small application, this is completely fine.
But our Community Information Platform will not remain this small.
That creates our next architectural problem.
Backend Development Series
This article is Update 4 of our step-by-step Backend Development series.
← Update 3 — Build Your First Express Server · Series Overview
The Problem With Keeping Everything Inside server.js
Right now, we only have:
textGET /But our Community Platform will eventually need Posts.
That may introduce endpoints such as:
textGET /api/postsGET /api/posts/:idPOST /api/postsPATCH /api/posts/:idThen Users may introduce:
textGET /api/usersGET /api/users/:idPOST /api/usersPATCH /api/users/:idAuthentication could eventually introduce:
textPOST /api/auth/registerPOST /api/auth/loginPOST /api/auth/logoutNotifications could introduce even more endpoints.
If we place all of these directly inside:
textserver.jsthe file may eventually contain dozens of routes.
Conceptually:
jsapp.get("/api/posts", ...);app.get("/api/posts/:id", ...);app.post("/api/posts", ...);app.patch("/api/posts/:id", ...);That might technically work.
But something working does not automatically mean it is easy to maintain.
A New Question Appears
We now ask:
Should
server.jsbe responsible for knowing every individual endpoint in the entire application?
For a small application, perhaps.
For a growing application, that becomes increasingly difficult to manage.
This gives us a genuine reason to introduce a new concept:
textROUTESAnd a new folder:
textroutes/We are not creating this folder simply because professional backend projects have one.
We are creating it because we have encountered a specific organizational problem.
What Is an API Route?
An API route tells our backend:
When a particular HTTP method reaches a particular path, which code should handle the request?
A route can be thought of as:
textHTTP Method + URL PathFor example:
textGET + /api/postscreates:
textGET /api/postsThis could mean:
Retrieve all Posts.
Another example:
textGET + /api/posts/5could mean:
Retrieve Post number 5.
Similarly:
textPOST /api/postscould mean:
Create a new Post.
HTTP Methods Describe the Action
The URL tells us which resource we are working with.
The HTTP method helps communicate what kind of action we want to perform.
For example:
Method | Example | Meaning |
|---|---|---|
|
| Retrieve Posts |
We are not implementing all of these operations yet.
First, we want to organize where Post-related routes belong.
Introducing Express Router
Express provides:
jsexpress.Router()A Router allows us to create a smaller collection of related routes.
Think of our main Express application as the entire building:
textExpress ApplicationInside that building, we can have different departments:
textExpress Application│├── Posts Router├── Users Router├── Authentication Router└── Notifications RouterEach Router focuses on one part of the application.
For now, we only need:
textExpress Application│└── Posts Router
Creating the routes Folder
Inside:
textsrc/we create:
textroutes/Our project becomes:
textcommunity-platform-api/│├── src/│ ├── routes/│ └── server.js│├── .gitignore├── package.jsonThis folder will contain route definitions.
But we still do not need to create:
textcontrollers/services/middleware/validators/utils/There is no reason to introduce those yet.
We introduce architecture when there is a problem that requires it.
Creating Our Post Routes File
Inside:
textsrc/routes/create:
textpost.routes.jsOur structure now becomes:
textsrc/├── routes/│ └── post.routes.js│└── server.jsThis file will be responsible for routes related to:
textPostsLater, our routes folder may grow into something like:
textroutes/├── post.routes.js├── user.routes.js├── auth.routes.js└── notification.routes.jsBut again:
Do not create those files until the application actually needs them.
Creating the Router
Inside:
textsrc/routes/post.routes.jswe start with:
jsimport express from "express";const router = express.Router();This looks similar to what we did inside server.js:
jsconst app = express();But they represent slightly different things.
app vs router
This:
jsconst app = express();represents our main Express application.
Think:
textWHOLE BACKENDWhile:
jsconst router = express.Router();creates a smaller router for related endpoints.
Think:
textONE SECTION OF THE BACKENDConceptually:
textapp│├── postRoutes├── userRoutes└── authRoutesFor now:
textapp│└── postRoutes
Creating Our First Post Endpoint
Inside post.routes.js, add:
jsrouter.get("/", (req, res) => { res.json({ message: "Here are all community posts", });});The file now looks like:
jsimport express from "express";const router = express.Router();router.get("/", (req, res) => { res.json({ message: "Here are all community posts", });Notice that we wrote:
jsrouter.get("/")rather than:
jsapp.get("/api/posts")There is a reason for this.
We are going to connect this Router to the main application using a base path.
Exporting the Router
Our Router currently exists only inside:
textpost.routes.jsBut server.js needs to use it.
So at the bottom we add:
jsexport default router;The complete file becomes:
jsimport express from "express";const router = express.Router();router.get("/", (req, res) => { res.json({ message: "Here are all community posts", });This means:
Allow another module to import this Router.
Importing the Router Into server.js
Open:
textsrc/server.jsand import the Post Router:
jsimport postRoutes from "./routes/post.routes.js";Our imports may now look like:
jsimport express from "express";import postRoutes from "./routes/post.routes.js";Because:
textpost.routes.jsis one of our own files, we provide the relative path.
Connecting the Post Router
Now we tell the main Express application where the Post Router should be mounted.
Add:
jsapp.use("/api/posts", postRoutes);Our server.js can now look like:
jsimport express from "express";import postRoutes from "./routes/post.routes.js";const app = express();const PORT = 3000;This line is extremely important:
jsapp.use("/api/posts", postRoutes);
Understanding app.use()
We are effectively telling Express:
Whenever a request begins with
/api/posts, send it to the Posts Router.
Inside our Router we have:
jsrouter.get("/")Express combines the two pieces.
The main application contributes:
text/api/postsThe Router contributes:
text/Together:
text/api/posts + /becomes:
text/api/postsTherefore:
textGET /api/postsmatches:
jsrouter.get("/")inside post.routes.js.
Follow the Request
Suppose the browser or another client requests:
textGET http://localhost:3000/api/postsThe request now travels through our application like this:
textCLIENT │ │ GET /api/posts ↓server.js │ │ app.use("/api/posts", postRoutes) ↓This is a significant improvement over putting every endpoint directly inside server.js.
Why Use /api/posts?
Technically, we could use:
text/postsBut using:
text/api/postsmakes it clear that this path belongs to our backend API.
For example, a full application may eventually have:
textWebsitehttps://community.example.comwhile API endpoints may use:
texthttps://community.example.com/api/postsDuring local development we use:
texthttp://localhost:3000/api/posts
Testing the Route
Run the server:
bashpnpm devThen visit:
texthttp://localhost:3000/api/postsYou should receive:
json{ "message": "Here are all community posts"}Our original root endpoint should also continue working:
texthttp://localhost:3000So we now have:
textGET /and:
textGET /api/postshandled separately.
Making the Response Look More Realistic
We do not have PostgreSQL yet.
For now, we can temporarily return JavaScript data.
For example:
jsrouter.get("/", (req, res) => { const posts = [ { id: 1, title: "Community Cleanup Exercise", shortDescription: "Community cleanup scheduled for Saturday.", }, {Visiting:
textGET /api/postscould then return:
json{ "message": "Posts retrieved successfully", "data": [ { "id": 1, "title": "Community Cleanup Exercise", "shortDescription": "Community cleanup scheduled for Saturday." },This is still temporary information.
It is not stored in a database yet.
That is okay.
Our current goal is understanding how requests are organized.
Why We Have Not Created Controllers Yet
You may have seen codebases that immediately contain:
textcontrollers/Why haven't we created that folder?
Because our route currently contains very little logic.
This:
jsrouter.get("/", (req, res) => { res.json(...);});is still manageable.
But soon we will add:
textGET all PostsGET one PostCREATE PostUPDATE PostThen the route file will start becoming much larger.
At that point we will ask:
Should route definitions also contain all of our application logic?
That problem will eventually give us a real reason to introduce:
textcontrollers/
Architecture Should Grow With the Application
Our project originally looked like:
textsrc/└── server.jsThen we encountered a problem:
textMore endpoints are coming ↓server.js could become overloadedSo we introduced:
textsrc/├── routes/│ └── post.routes.js│└── server.jsThis gives us a useful development principle:
Do not add complexity before you understand the problem that complexity is solving.
The Mental Model
Our application now has another layer.
Before:
textCLIENT ↓server.js ↓ResponseNow:
textCLIENT ↓server.js ↓routes/ ↓Route Handler ↓Later this may become:
textCLIENT ↓server.js ↓routes/ ↓controllers/ ↓But we will reach those layers gradually.
What We Learned
In this update, we learned why routes deserve their own place in a growing backend.
We introduced:
- API Routes
- HTTP methods
- URL paths
express.Router()routes/post.routes.js
Most importantly, we learned why the routes/ folder exists.
Not because a tutorial told us to create it.
Because:
textserver.jswas beginning to take responsibility for too many endpoints.
Our Project Structure After Update 4
At this stage:
textcommunity-platform-api/│├── node_modules/│├── src/│ ├── routes/│ │ └── post.routes.js│ │Still simple.
But more organized than before.
The Bigger Lesson
Software architecture is often a response to growing complexity.
Our progression currently looks like:
textReal-World Problem ↓Node.js Project ↓Express Server ↓More API Endpoints ↓We are not memorizing a finished architecture.
We are watching that architecture appear naturally.
Continue Learning
You have completed Update 4 — Organizing Your Express API with Routes.
Previous
← Update 3 — Build Your First Express Server with Beginner JavaScript
Coming Next
Update 5 — Building CRUD Operations for Our Posts
Now that our Post endpoints have their own Router, the next question is:
What should those endpoints actually allow the application to do?
In the next lesson, we will introduce:
GETPOSTPATCHDELETE- CRUD
View the Full Backend Development Series
Browse All GREE Software Company Updates
GREE Software Academy
At GREE Software Academy, the goal is not simply to memorize what folders belong inside a backend application.
We want to understand why those folders become necessary.
textEncounter a Problem ↓Understand It ↓Introduce a Solution ↓Understand the Solution ↓Today, that problem was endpoint organization.
The solution was routing.
Next, those routes will begin performing real CRUD operations.
Start small. Understand why. Then scale.

