![How to build basic Express.js from scratch [Part 1] - hero img](/_next/image?url=https%3A%2F%2Fpublic-r2.ticklab.site%2Fmedia%2Fhero.png%3F2025-06-03T15%3A07%3A14.634Z&w=3840&q=100)
How to build basic Express.js from scratch [Part 1]
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 -v2npm -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 libraries3│-- public/ # Contains static files (images, CSS, JS, HTML)4│-- src/5│ │-- routes/ # Contains API route definitions6│ │-- middleware/ # Contains custom middleware7│ │-- static/ # Renders static files8│ │-- app.js # Application configuration file9│-- index.js # Server entry point10│-- package.json # Package configuration file11│-- 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 usingnpm run <script_command>
.start
: Runs the server usingnodemon 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) usesrequire()
andmodule.exports
."module"
uses ESM (ECMAScript Module) withimport
andexport
.
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 themiddlewares
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:
Theget
andpost
methods allow users to register routes for the corresponding HTTP methods. Each call tothis.router.addRoute
stores a route and its handler, enabling the framework to automatically dispatch requests based on HTTP method and path. (We’ll only implementget
andpost
here, but you can read more about HTTP methods likeput
anddelete
as well.)
1get(path, handler) {2 this.router.addRoute("GET", path, handler);3}45post(path, handler) {6 this.router.addRoute("POST", path, handler);7}
listen
method:
This method creates an HTTP server usinghttp.createServer
. The request handler runs the middleware chain by invoking each middleware function in order using anext()
function. The flow works like this:
- If there are more middleware functions left, call the next one.
- 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);1314}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:
TheaddRoute(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:
Thehandle(req, res)
method performs the following steps: - Extract pathname: Create a
URL
object fromreq.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.
- Extract pathname: Create a
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:

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
- Express.js Documentation, https://expressjs.com/
- Node.js HTTP Module API, https://nodejs.org/api/http.html
- How To Use And Write Express Middleware, https://blog.webdevsimplified.com/2019-12/express-middleware-in-depth/