Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
Introduction
Node.js is a popular platform that enables fast and scalable server-side development. And Express is a powerful web framework that lets you get even more out of Node.js. In this article, we’ll explain how to develop web applications using Express, aimed at beginners. Even if you’re just getting started or have never touched Express before, you can follow along with confidence. We’ll go step by step from setting up Express, through routing and the basics of creating APIs, so you can gain practical knowledge.
Goal for This Article
We’ll confirm that when we access an Express server with GET and POST methods, we receive responses correctly.
We’ll send requests to the server using the curl command.
In addition to routing, we’ll also implement and dig into topics that are more useful in real-world projects, such as:
- How to handle cases where query parameters are set
- Using middleware to obtain access logs
- Common error handling
What Is Express?
Express is a lightweight and flexible web application framework used with Node.js. It mainly provides a toolset that makes server-side application development easier. With Express, you can easily implement the basic features needed for a web server, such as handling HTTP requests, routing, managing middleware (functions that operate on requests and responses), and supporting template engines.
Features of Express
-
Simple and lightweight
Express is very simple and only provides the minimum required features. Because of that, it’s easy to extend freely according to your project, and the learning cost is low. -
Routing functionality
Express lets you easily define routes (endpoints) that correspond to requests (GET, POST, PUT, DELETE, etc.) for specific URLs. -
Use of middleware
Middleware are functions that run before processing a request. They allow you to centralize common processing for requests, such as authentication, logging, error handling, and data parsing. -
Template engine support
Express integrates easily with template engines (such as EJS or Pug) that support dynamic HTML generation, allowing you to generate HTML pages dynamically. -
Support for asynchronous processing
Express runs on Node.js and supports asynchronous I/O. This gives it high performance and allows it to handle many requests simultaneously.
Benefits of Using Express
-
Quickly spin up applications
By using Express, you can reduce the time spent on setup and preparation and start developing servers or APIs right away. -
Suitable for large-scale applications as well
Express can handle everything from small to large-scale applications and is highly extensible. Many plugins and middleware are provided officially and by the community. -
Widely used
Express is extremely popular in the Node.js ecosystem and is used by many developers. As a result, there are abundant resources and documentation, making it easy to learn.
Official Express site
Introducing Express
First, create a package.json.
mkdir express-basics-guide
cd express-basics-guide
npm init -y
Next, install Express.
npm i express
That’s all you need to introduce Express.
Creating Sample Code
We’ll write a simple piece of code to check that Express is working correctly.
Create a file named server.js in the project root.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello, Express');
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Here’s an explanation of the code.
const express = require('express');
const app = express();
We execute the function provided by Express and generate an instance for the server.
app.get('/', (req, res) => {
res.send('Hello, Express');
});
When / is accessed with the GET method, it returns the string Hello, Express.
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
It starts on port 3000 and outputs a console log after startup so we can confirm that it’s running.
Verifying Operation
Start Express.
node server.js
It looks like it started successfully.
If you access http://localhost:3000/ in your browser, you can confirm that the string we set earlier is displayed.
If you find it tedious to check in the browser every time, you can use curl instead.
curl http://localhost:3000/
Development with TypeScript
Now that you have an idea of how simple Express is, let’s gradually dig deeper.
First, we’ll configure the project so we can write code in TypeScript.
Installing Required Packages
Install the packages needed to run with TypeScript.
npm i -D typescript @types/node @types/express ts-node
-
typescript
The TypeScript compiler. It’s required to transpile TypeScript code to JavaScript. This package is essential when using TypeScript in your project. -
@types/node
A type definition package for Node.js. It provides type checking and auto-completion when using Node.js standard libraries (fs, http, path, etc.) in TypeScript. -
@types/express
A type definition package for Express. It provides type information for routes and middleware when using the Express framework with TypeScript, supporting auto-completion and type checking. -
ts-node
A tool that allows you to run TypeScript code directly. Normally, you compile TypeScript code before running it, but with ts-node you can run it without pre-compiling. It’s convenient for running scripts during development.
Creating tsconfig
Create a tsconfig.json file to manage compiler settings for the TypeScript project.
npx tsc --init
Change js -> ts
Change the extension from .js to .ts and adjust the code.
- const express = require('express');
+ import express from 'express'
const app = express();
app.get('/', (_req, res) => {
res.send('Hello, Express');
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
If you check in your editor, you can see that types are defined.
Verifying Operation
Start Express with ts-node.
npx ts-node server.ts
If you access it with curl and the configured string is returned, you’re good.
curl http://localhost:3000/
Watching for Changes
Running the command to start Express every time you make a change is a hassle during development, so we’ll use a file-watching tool.
npm i -D nodemon
Since the startup command will be long, we’ll define it in package.json.
"scripts": {
+ "dev": "nodemon --watch '*.ts' --exec 'ts-node' server.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
You can now run it with the following command:
npm run dev
If you update server.ts (for example, change the response content) and save the file, Express will automatically restart.
This makes it easier to develop with Express.
Routing
Here are some examples of routing, one of the features of Express.
We’ll assume you’re adding these to server.ts.
GET Method
This makes /user accessible with the GET method.
app.get('/user', (req, res) => {
res.send('Hello, User');
});
curl http://localhost:3000/user
Hello, User
POST Method
You can create a POST method with app.post().
app.post('/submit', (req, res) => {
res.send('Form submitted!');
});
url -X POST http://localhost:3000/submit
Form submitted!
Routing with Parameters
You can include dynamic parameters in the URL.
You can access parameters via req.params, so you can return responses based on the request.
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});
curl http://localhost:3000/users/123
User ID: 123
Query Parameters
This is how to handle URLs that include query strings.
You can access parameters via req.params, so you can return responses based on the query parameters.
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`Search query: ${query}`);
});
curl "http://localhost:3000/search?q=express"
Search query: express
Grouping Routes
If you define many routes, things can get complicated and maintainability will drop.
If possible, you can improve maintainability by grouping routes.
const router = express.Router();
router.get('/profile', (req, res) => {
res.send('User profile');
});
router.get('/settings', (req, res) => {
res.send('User settings');
});
app.use('/user', router);
curl http://localhost:3000/user/profile
User profile
curl http://localhost:3000/user/settings
User settings%
app.get('/test', (req, res) => {
res.send('Hello from Handler 1');
});
app.get('/test', (req, res) => {
res.send('Hello from Handler 2');
});
curl http://localhost:3000/test
Hello from Handler 1
Middleware
Express middleware are functions that perform additional processing between the request and the response.
By using middleware, you can extend your application’s functionality with things like request logging, authentication, data transformation, and error handling.
Characteristics of Middleware
-
Can manipulate requests and responses
You can modify the request (req) object or perform additional processing on the response (res) object. -
Pass processing along in a chain
By callingnext(), you pass control to the next middleware or route. If you don’t call it, processing stops. -
Order is important
Middleware is executed in the order it’s registered. If you get the order wrong, behavior may be incorrect.
Application-Level Middleware
Use this when you want to configure something common to all processing.
Here, we output the request method and URL on the server side to record simple logs.
// Middleware for logging
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
You can confirm that logs are output on the server side according to the requests.
curl http://localhost:3000/
curl http://localhost:3000/test
curl -X POST http://localhost:3000/submit
Router (Route)-Level Middleware
Middleware that applies only to specific routes.
app.get('/user/:id', (req, res, next) => {
console.log(`Request for user ID: ${req.params.id}`);
next();
}, (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
curl http://localhost:3000/user/123
User ID: 123
The following log is output on the server you started:
Request for user ID: 123
Built-in Middleware
Express has some convenient built-in middleware.
express.json() parses JSON request bodies.
// Set JSON parsing middleware
app.use(express.json());
app.post('/api/data', (req, res) => {
console.log(req.body); // Parsed JSON data
res.send(`Received: ${JSON.stringify(req.body)}`);
});
curl -X POST http://localhost:3000/api/data \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 25}'
Received: {"name":"Alice","age":25}
If you send incorrectly formatted JSON, you’ll get an error.
curl -X POST http://localhost:3000/api/data \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 25,'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at parse (/Users/test/Documents/workspace/express-basics-guide/node_modules/body-parser/lib/types/json.js:92:19)<br> at /Users/test/Documents/workspace/express-basics-guide/node_modules/body-parser/lib/read.js:128:18<br> at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> at invokeCallback (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:238:16)<br> at done (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:227:7)<br> at IncomingMessage.onEnd (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:287:7)<br> at IncomingMessage.emit (node:events:517:28)<br> at IncomingMessage.emit (node:domain:489:12)<br> at endReadableNT (node:internal/streams/readable:1400:12)</pre>
</body>
</html>
Points to Note
Error Handling
In the previous section, when the JSON format was invalid, an error message was returned.
You can check the error content there as well, but generally you define error handling.
import express, { ErrorRequestHandler } from 'express'
// Custom error handling
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
if (err instanceof SyntaxError) {
res.status(400).send({ error: 'Invalid JSON format' });
return
}
next(err);
};
app.use(errorHandler);
We defined the error message for when a SyntaxError occurs.
curl -X POST http://localhost:3000/api/data -H "Content-Type: application/json" -d '{"name": "Alice", "age": 25,'
{"error":"Invalid JSON format"}
We’ve now handled syntax errors, but in real applications, errors can occur for many reasons, and there are cases where you can’t return a proper response.
A simple example:
app.get('/error', (req, res) => {
throw new Error('Something went wrong!');
});
curl http://localhost:3000/error
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: Something went wrong!<br> at /Users/test/Documents/workspace/express-basics-guide/server.ts:74:9<br> at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:95:5)<br> at next (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/route.js:149:13)<br> at Route.dispatch (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/route.js:119:3)<br> at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:95:5)<br> at /Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:284:15<br> at Function.process_params (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:346:12)<br> at next (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:280:10)<br> at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:91:12)<br> at trim_prefix (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:328:13)</pre>
</body>
</html>
To handle cases like this, we’ll prepare generic error handling.
// Custom error handling
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
if (err instanceof SyntaxError) {
res.status(400).send({ error: 'Invalid JSON format' });
return
}
- next(err);
+ console.error(err); // Record error logs (useful during development)
+ res.status(500).send('Internal Server Error')
+ return
};
app.use(errorHandler);
This error-handling middleware should be written after routing such as app.get. Also, it needs to be defined so that it’s called last among app.use calls.
curl http://localhost:3000/error
{"error":"Internal Server Error"}
Here is the server.ts we’ve written so far.
server.ts
import express, { ErrorRequestHandler } from 'express'
const app = express();
const router = express.Router();
// Middleware for logging
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
// Set JSON parsing middleware
app.use(express.json());
app.get('/user/:id', (req, res, next) => {
console.log(`Request for user ID: ${req.params.id}`);
next();
}, (req, res) => {
res.send(`User ID: ${req.params.id}`);
});
app.get('/', (req, res) => {
res.send('Hello, Express');
});
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
res.send(`User ID: ${userId}`);
});
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`Search query: ${query}`);
});
app.post('/submit', (req, res) => {
res.send('Form submitted!');
});
app.post('/api/data', (req, res) => {
console.log(req.body);
res.send(`Received: ${JSON.stringify(req.body)}`);
});
router.get('/profile', (req, res) => {
res.send('User profile');
});
router.get('/settings', (req, res) => {
res.send('User settings');
});
app.use('/user', router);
app.get('/test', (req, res) => {
res.send('Hello from Handler 1');
});
app.get('/test', (req, res) => {
res.send('Hello from Handler 2');
});
app.get('/error', (req, res) => {
throw new Error('Something went wrong!');
});
// Custom error handling
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
if (err instanceof SyntaxError) {
res.status(400).send({ error: 'Invalid JSON format' });
return
}
console.error(err); // Record error logs (useful during development)
res.status(500).send({ error: 'Internal Server Error' })
return
};
app.use(errorHandler);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Recommended Articles
Conclusion
In this article, we covered the basics of web application development using the Express framework for Node.js. The appeal of Express lies in the fact that it’s simple yet has very powerful features. Try introducing Express into your own projects using this article as a reference. To really master it for actual development, it’s important to work on a few practical projects. Keep learning Node.js and Express to hone your efficient web development skills.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
2024/12/09Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Test Automation with Jest and TypeScript: A Complete Guide from Basic Setup to Writing Type-Safe Tests
2023/09/13ESLint / Prettier Introduction Guide: Thorough Explanation from Husky, CI/CD Integration, to Visualizing Code Quality
2024/02/12Practical Microservices Strategy: The Tech Stack Behind BFF, API Management, and Authentication Platform (AWS, Keycloak, gRPC, Kafka)
2024/03/22Building a Mock Server for Frontend Development: A Practical Guide Using @graphql-tools/mock and Faker
2024/12/30Streamlining API Mocking and Testing with Mock Service Worker (MSW)
2023/09/25





