← HomeSee All Blogs

How I Learned to Love Express, With TypeScript

2025-11-30

Introduction

So lately, I’ve been going deeper into Express, and honestly?
I started appreciating it way more once I stripped away all the extras — no Prisma, no Postgres, no noise.
Just Express, TypeScript, and intentional structure.

It felt like learning the language of the backend for real.

I stopped thinking of Express as “just a simple server” and started seeing it as a flexible little engine that powers pretty much any front-end I throw at it — React, Next.js, mobile apps, whatever.


Why TypeScript + Express Just Makes Sense

At first, I thought mixing TypeScript with Express would slow me down.
But then it clicked:

Request and Response types help prevent dumb mistakes

It’s like Express grows up a bit when TypeScript enters the room.

So I made a simple project:

npm init -y
npm install express cors
npm install -D typescript ts-node-dev @types/node @types/express
npx tsc --init

Then I flipped the switch:

{
    "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true
    }
}

Instantly, everything felt structured.


The Backend Structure That Just Works

I kept coming back to this layout because it scales without becoming a mess:

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

It’s simple but disciplined.

This structure prevents everything ending up in index.ts like a doomed spaghetti experiment.


Controllers: Where I Stopped Mixing Logic With Responses

A controller should answer a request, nothing more.

// controllers/userController.ts
import { Request, Response } from "express";
import * as userService from "../services/userService";

export const getAllUsers = async (req: Request, res: Response) => {
    const users = await userService.fetchUsers();
    res.json(users);
};

No database code, no validation logic.
Just a simple bridge between Express and your service layer.


Services: My Favorite Layer

Once I separated services, everything felt cleaner.

// services/userService.ts
export const fetchUsers = async () => {
    return [
        { id: 1, name: "Satoshi" },
        { id: 2, name: "Ada" }
    ];
};

The service layer is where the “thinking” happens.
Validation, transformations, DB calls — all here.


Routes: The Map of My API

// routes/userRoutes.ts
import { Router } from "express";
import { getAllUsers } from "../controllers/userController";

const router = Router();

router.get("/", getAllUsers);

export default router;

Simple. Readable. Scalable.


The Index File: Where the Magic Starts

This is where I fixed the classic mistakes:

// index.ts
import express from "express";
import cors from "cors";
import userRoutes from "./routes/userRoutes";

const app = express();

app.use(express.json());
app.use(cors());

app.use("/users", userRoutes);

app.listen(3000, () => console.log("Server running on port 3000"));

Adding express.json() feels like unlocking a secret door — suddenly your API accepts JSON like a civilized human.


Why This Fits Perfectly With React or Next.js

React and Next love predictable, well-structured APIs.

When Express is modular and typed:

React/Next asks → Express answers.
Life is good.


Common Mistakes I Stopped Making

Once I stopped doing these, everything felt cleaner.


Tips I Wish Someone Told Me Earlier


Final Thoughts

This Express + TypeScript phase felt like a real leveling-up moment.

It wasn’t flashy.
It wasn’t trendy.
Just clean architecture, clear boundaries, and this peaceful sense that everything is finally “in its place.”

Now Express feels like a quiet, dependable foundation under all my React and Next apps.

Honestly?
That’s when you really get Express —
when it disappears and simply supports your whole stack without getting in the way.

You may also like