TickLab LogoTickLab
How to build basic Express.js from scratch [Part 1] - hero img

How to build basic Express.js from scratch [Part 1]

Posted on Jun 02, 2025

If you're already familiar with creating interfaces using HTML, CSS, and JavaScript and now want to build the backend for your website, then Express.js is the perfect choice. In this article, we will go through the journey of building a basic Express.js framework from scratch. Throughout the process, you will learn how Express.js handles routing, middleware, and HTTP requests, enabling smooth and flexible connections between the user interface and the server system.

1. What is Node.js? Why choose Express.js?

Node.js is a platform that runs JavaScript on the server side, allowing developers to use JavaScript not only in the browser but also on the backend. With its asynchronous processing capabilities and high performance, Node.js has become a popular choice for building web applications, APIs, microservices, and more. However, Node.js itself does not provide a built-in framework to simplify web application development. This is exactly why Express.js was created.

Express.js is a lightweight yet powerful framework for Node.js that makes building web applications and APIs much simpler. It allows for intuitive routing of HTTP requests and provides a middleware mechanism to extend functionality—such as authentication, logging, and error handling—with ease. As a result, the connection between the user interface and the server system becomes smoother than ever.

To be able to rebuild Express.js from scratch, you’ll need to equip yourself with some essential foundational knowledge. First, understand how Node.js works—from the event loop to non-blocking I/O—along with module management and JavaScript programming concepts (such as async/await, arrow functions, etc.). In addition, a solid understanding of the HTTP protocol, methods like GET, POST, PUT, DELETE, and how request and response objects work will help you implement routing and request handling accurately. Lastly, I recommend spending a bit of time getting started with Express.js so that the coding process will be smoother.

2. Environment Setup

2.1. Installing Node.js

Express.js is built on top of Node.js, so you need to install Node.js first. You can download Node.js from the official website: Node.js.org. After installation, open your terminal and check the versions of Node.js and npm with the following commands:

1node -v
2npm -v

If the versions are displayed, it means Node.js has been successfully installed.

2.2. Initializing the Project

Next, you need to create a directory for your project and initialize a new Node.js project using npm. Open your terminal and run the following commands:

1mkdir <your_project_name>
2cd <your_project_name>
3npm init -y

The npm init -y command will create a package.json file with default settings, helping you manage the packages required for your project. Alternatively, you can use the npm init command and manually enter your configuration details.

2.3. Installing Additional Packages

In Node.js, there are several built-in modules called core modules, such as http, url, querystring, etc. This means you can use them right away without installing them via npm. However, for nodemon – a useful tool that automatically restarts the server when code changes – you need to install it separately, as it is not part of Node.js core modules. You can install nodemon with the following command:

1npm install --save-dev nodemon

When you install a package with the --save-dev flag, it will be added to the devDependencies section in the package.json file. This means the package is only used during development and is not needed when the application is running in a production environment. For example, nodemon is useful during development because, in production, you would run the server using Node.js without the need to automatically restart the code.

2.4. Project Directory Structure

The directory structure below helps you organize your source code clearly and makes it easier to scale. Details about the folder organization and the role of each component will be explained in the following sections:

1your-project-name/
2│-- node_modules/ # Installed libraries
3│-- public/ # Contains static files (images, CSS, JS, HTML)
4│-- src/
5│ │-- routes/ # Contains API route definitions
6│ │-- middleware/ # Contains custom middleware
7│ │-- static/ # Renders static files
8│ │-- app.js # Application configuration file
9│-- index.js # Server entry point
10│-- package.json # Package configuration file
11│-- package-lock.json # Package version lock file

2.5. Configuring the package.json File

After running the npm init -y command, a package.json file will be generated with the following default content:

1{
2 "name": "your-project-name",
3 "version": "1.0.0",
4 "main": "index.js",
5 "scripts": {
6 "test": "echo \"Error: no test specified\" && exit 1"
7 },
8 "keywords": [],
9 "author": "",
10 "license": "ISC",
11 "type": "commonjs",
12 "description": ""
13}

To make development easier, we need to edit the scripts section to add a command to run the server using nodemon:

1"scripts": {
2 "start": "nodemon index.js"
3}

Explanation of each line in the package.json file:

  • name: The name of the project.
  • version: The current version of the project.
  • main: The main entry file of the project.
  • scripts: Contains commands that can be run using npm run <script_command>.
    • start: Runs the server using nodemon index.js, automatically restarting on changes.
  • keywords: A list of keywords describing the project.
  • author: The author of the project.
  • license: The license under which the project is distributed.
  • type: Defines how Node.js handles modules.
    • "commonjs" (default) uses require() and module.exports.
    • "module" uses ESM (ECMAScript Module) with import and export.
  • description: A short description of the project.

After updating, you can start the server by running:

1npm run start

With this step, the project is now basically set up and ready for the next stages in building your own Express.js framework.

3. Building the Application

After finishing the environment setup, we’ll begin building the App class — the core of the framework. This class not only initializes the HTTP server but also integrates the routing system (Router) and handles middleware in a simple yet effective way. We’ll start coding in the app.js file that we created in section 2.4.

3.1. Import the required modules

You need to import Node.js’s built-in http module to create the server. Additionally, the Router module (which we will build later) will be responsible for managing routes and mapping incoming HTTP requests to their corresponding handler functions.

3.2. Initializing the App

In the constructor, we create an instance of Router and an array named middlewares to store user-defined middleware. This makes it easy to plug in cross-cutting functionalities (like logging, body parsing, authentication, etc.) before the request reaches the route handler. If you’re unfamiliar with middleware, you can learn more about it here.

1class App {
2 constructor() {
3 this.router = new Router();
4 this.middlewares = [];
5 }
6}

3.3. Building the Methods

  • use method:
    This method allows adding middleware to the application. Each registered middleware is pushed into the middlewares array. This mechanism works similarly to Express, allowing additional functionality to be hooked into every request.
1use(middleware) {
2 this.middlewares.push(middleware);
3}
  • Registering routes:
    The get and post methods allow users to register routes for the corresponding HTTP methods. Each call to this.router.addRoute stores a route and its handler, enabling the framework to automatically dispatch requests based on HTTP method and path. (We’ll only implement get and post here, but you can read more about HTTP methods like put and delete as well.)
1get(path, handler) {
2 this.router.addRoute("GET", path, handler);
3}
4
5post(path, handler) {
6 this.router.addRoute("POST", path, handler);
7}
  • listen method:
    This method creates an HTTP server using http.createServer. The request handler runs the middleware chain by invoking each middleware function in order using a next() function. The flow works like this:
  1. If there are more middleware functions left, call the next one.
  2. Once all middleware has run, pass control to this.router.handle(req, res) to dispatch the request to the appropriate route handler.
    Finally, server.listen starts the server on the specified port and invokes the callback when the server is ready. You can read more about the HTTP module here.
1listen(port, callback) {
2 const server = http.createServer((req, res) => {
3 let i = 0;
4 const next = () => {
5 if (i < this.middlewares.length) {
6 return this.middlewares[i++](req, res, next);
7 }
8 this.router.handle(req, res);
9 };
10 next();
11 });
12 server.listen(port, callback);
13
14}
15module.exports = App;

3.4. Summary

In this section, we built the App class — the core component of the framework — responsible for starting the server, managing middleware, and handling basic routing. Next, we’ll proceed to the crucial step of building the Router system, which directs requests to their appropriate handlers.

4. The Router System

In this section, we’ll build a simple routing system (Router) that handles incoming HTTP requests and dispatches them to the appropriate handler functions based on the HTTP method and path. We’ll create a file called router.js inside the routes folder, which we created in section 2.4.

4.1. Initializing the Router

We’ll define a Router class with a routes object to store all routes, grouped by HTTP method.

1class Router {
2 constructor() {
3 this.routes = {};
4 }
5}

The structure of the routes object looks like this:

1{
2 "GET": { "/": handlerFunction, "/about": handlerFunction },
3 "POST": { "/login": handlerFunction }
4}

This grouping allows for efficient and organized route lookup based on HTTP methods.

4.2. Building the Methods

  • The addRoute method:
    The addRoute(method, path, handler) method lets you register a new route:
    • method: HTTP method such as GET, POST, PUT, DELETE, etc.
    • path: The path that the client sends the request to.
    • handler: The function that handles the request when the route matches.
    • If no route group exists for that method yet, we initialize it with an empty object. Then, the route is saved to this.routes under the corresponding path.
1addRoute(method, path, handler) {
2 if (!this.routes[method]) {
3 this.routes[method] = {};
4 }
5 this.routes[method][path] = handler;
6}
  • The handle method:
    The handle(req, res) method performs the following steps:
    • Extract pathname: Create a URL object from req.url to get the pathname of the request. This allows us to focus only on the path without being affected by the query string.
    • Get HTTP method: Use req.method to determine the request method.
    • Route lookup: Check whether a handler is registered for the given method and path.
    • Call handler or return 404 error: If a handler is found, call it and return the response. If not, set a 404 status code and send a "Not Found" message.
1handle(req, res) {
2 const { pathname } = new URL(req.url, `http://${req.headers.host}`);
3 const method = req.method;
4 if (this.routes[method] && this.routes[method][pathname]) {
5 return this.routes[method][pathname](req, res);
6 }
7 res.statusCode = 404;
8 res.end('Not Found');
9}
10

4.3. Summary

In this section, we built a simple Router system from scratch to handle HTTP requests by mapping methods and paths to the corresponding handler functions. While this is a basic version, it demonstrates clearly how a routing system works in Express.js. In the next section, we’ll set up the index.js file (entry point) to test whether our server is functioning correctly.

5. Setting Up the Entry Point

After building the Router system, the next step is to set up the index.js file as the entry point for the application. This file is responsible for starting the server and registering routes to test whether our system is working correctly.

5.1. Initializing the Application

First, we need to import the App class from the src directory, which contains all our request-handling and routing logic. Then, we create an instance of App, which serves as the main server of the application:

1const App = require("./src/app");
2const app = new App();

5.2. Defining a Route

Use the get() method to register a simple route at /, which returns the string "Hello World" when a request is received:

1app.get("/", (req, res) => {
2 res.end("Hello World");
3});

5.3. Starting the Server

Finally, call the listen(port, callback) method to start the server and listen for incoming requests on port 3000. Once the server is running successfully, it will print a message to the console:

1app.listen(3000, () => {
2 console.log("Server running on http://localhost:3000");
3});

5.4. Result

After running the command:

1npm run start

You will see the following message in your terminal console:

terminal.png

When you visit http://localhost:3000/ in your browser or use cURL:

1curl http://localhost:3000/

You will receive the response:

1Hello World

This confirms that our server has been successfully initialized.

6. Part 1 Summary

In this part, we’ve gone through the fundamental steps of rebuilding Express.js from scratch, including:

  • Environment Setup: Installing Node.js and initializing the project.
  • Building the App: Creating the App class to manage the entire application.
  • Building the Router: Developing a simple routing system to handle requests based on HTTP methods and paths.
  • Starting the Server: Setting up the entry point file and running the server to verify route functionality.

These steps have helped you understand the basic mechanisms behind how Express.js works. In the next part, we’ll dive deeper into handling middleware and serving static files to extend the application’s capabilities.

You can follow the project’s progress and updates via the GitHub repository.

7. References