Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications

  • expressjs
    expressjs
  • typescript
    typescript
Published on 2024/12/07

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

  1. 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.

  2. Routing functionality
    Express lets you easily define routes (endpoints) that correspond to requests (GET, POST, PUT, DELETE, etc.) for specific URLs.

  3. 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.

  4. 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.

  5. 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

  1. 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.

  2. 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.

  3. 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

https://expressjs.com/

Introducing Express

First, create a package.json.

mkdir express-basics-guide
cd express-basics-guide
npm init -y

Image from Gyazo

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.

server.js
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.

Image from Gyazo

If you access http://localhost:3000/ in your browser, you can confirm that the string we set earlier is displayed.

Image from Gyazo

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
  1. typescript
    The TypeScript compiler. It’s required to transpile TypeScript code to JavaScript. This package is essential when using TypeScript in your project.

  2. @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.

  3. @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.

  4. 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.

server.ts
- 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.

Image from Gyazo

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.

Image from Gyazo

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

  1. Can manipulate requests and responses
    You can modify the request (req) object or perform additional processing on the response (res) object.

  2. Pass processing along in a chain
    By calling next(), you pass control to the next middleware or route. If you don’t call it, processing stops.

  3. 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

Image from Gyazo

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> &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at parse (/Users/test/Documents/workspace/express-basics-guide/node_modules/body-parser/lib/types/json.js:92:19)<br> &nbsp; &nbsp;at /Users/test/Documents/workspace/express-basics-guide/node_modules/body-parser/lib/read.js:128:18<br> &nbsp; &nbsp;at AsyncResource.runInAsyncScope (node:async_hooks:203:9)<br> &nbsp; &nbsp;at invokeCallback (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:238:16)<br> &nbsp; &nbsp;at done (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:227:7)<br> &nbsp; &nbsp;at IncomingMessage.onEnd (/Users/test/Documents/workspace/express-basics-guide/node_modules/raw-body/index.js:287:7)<br> &nbsp; &nbsp;at IncomingMessage.emit (node:events:517:28)<br> &nbsp; &nbsp;at IncomingMessage.emit (node:domain:489:12)<br> &nbsp; &nbsp;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> &nbsp; &nbsp;at /Users/test/Documents/workspace/express-basics-guide/server.ts:74:9<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/route.js:149:13)<br> &nbsp; &nbsp;at Route.dispatch (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/route.js:119:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:284:15<br> &nbsp; &nbsp;at Function.process_params (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:346:12)<br> &nbsp; &nbsp;at next (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/index.js:280:10)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/Users/test/Documents/workspace/express-basics-guide/node_modules/express/lib/router/layer.js:91:12)<br> &nbsp; &nbsp;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
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');
});

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.

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form