← HomeSee All Blogs

How I Structure Express Apps After Breaking Them 3 Times

2025-12-30

Introduction

I didn’t arrive at my Express project structure because I planned it perfectly.

I arrived there because I broke my backend three different times.

Each time, the app technically worked — until it didn’t.
Adding features felt risky.
Refactoring felt like pulling on a loose thread and hoping the sweater didn’t unravel.

This post is about what failed, what finally clicked, and the structure I now use that makes Express feel calm instead of fragile.


The First Break: Everything in One File

My earliest Express apps looked like this:

// index.ts
app.get("/users", async (req, res) => {
    // logic
});

app.post("/users", async (req, res) => {
    // more logic
});

At first, this felt fast.
No folders. No decisions. Just code.

But very quickly:

It worked — but only in the short term.

Lesson:
Express lets you do this, but it doesn’t mean it’s sustainable.


The Second Break: Fat Routes Everywhere

So I split things up.

src/
    routes/
        users.ts
        posts.ts

Better… but still messy.

router.post("/", async (req, res) => {
    // validation
    // business logic
    // database calls
    // response formatting
});

Routes started doing everything.

Soon:

Lesson:
Routes should describe where requests go — not how things work.


The Third Break: Controllers That Tried to Do Too Much

Next evolution: controllers.

Progress — but I still made them too powerful.

export const createUser = async (req, res) => {
    // validation
    // rules
    // prisma queries
    // formatting
};

Controllers became bloated.
Refactoring hurt again.
Everything felt tightly coupled.

Lesson:
Controllers are not the brain of your app — they’re the bridge.


The Structure I Finally Landed On

After breaking things enough times, this structure stuck:

src/
    routes/
    controllers/
    services/
    middleware/
    prisma/
    types/
    index.ts

Simple.
Predictable.
Scales without stress.

Each layer has one responsibility.


Routes: Just the Map

Routes define what exists, nothing more.

// routes/userRoutes.ts
router.get("/", getUsers);
router.post("/", createUser);

No logic.
No thinking.
Just mapping URLs to controllers.


Controllers: Request In, Response Out

Controllers are intentionally thin.

// controllers/userController.ts
export const createUser = async (req: Request, res: Response) => {
    const user = await userService.createUser(req.body);
    res.status(201).json(user);
};

They:

If a controller feels complex, something is off.


Services: Where the Thinking Lives

This is where everything clicked.

// services/userService.ts
export const createUser = async (data: CreateUserDTO) => {
    if (!data.email) {
        throw new Error("Email is required");
    }

    return prisma.user.create({ data });
};

Services:

This is where complexity belongs.


Middleware: Cross-Cutting Concerns

Auth.
Logging.
Rate limiting.
Validation.

app.use(authMiddleware);

Middleware keeps this logic out of your core app, which massively improves clarity.


Types: Quietly Saving Me From Myself

Adding a types/ folder was a turning point.

export interface CreateUserDTO {
    name: string;
    email: string;
}

Types:


Why This Structure Actually Scales

The biggest win wasn’t performance.

It was confidence.

I always know:

Adding features stopped feeling risky.


Mistakes I No Longer Make

Each one cost me time before I learned the pattern.


What I’d Tell My Past Self


Final Thoughts

I used to think backend structure was overengineering.

Now I see it as friction removal.

Express didn’t change.
I did.

Once I stopped fighting structure and started embracing it, Express stopped feeling fragile and started feeling dependable.

And honestly?
That’s when building backends started feeling fun again.

You may also like