Express is a common framework for NodeJS development. Here’s how to use it with Typescript.

The target

Our goal is to be able to quickly develop our applications using Typescript, and end up compiling our applications into raw JavaScript code to be executed by the NodeJS runtime.

Initialization Settings

The first thing we need to do is create a directory called express-typescript-app to store our project code:

mkdir express-typescript-app
cd express-typescript-app
Copy the code

To achieve our goal, we first need to distinguish between online program dependencies and development dependencies to ensure that the compiled code will be useful.

In this tutorial, you will use the YARN command as the package manager, as well as NPM.

Production environment dependency

Express, as the main framework for a program, is essential and needs to be installed in a production environment

yarn add express
Copy the code

This creates a package.json file in the current directory with only one dependency in it for now

Develop environment dependencies

In the development environment we will be writing code in Typescript. So we need to install typescript. You also need to install type declarations for Node and Express. Install with the -d parameter to ensure that it is a development dependency.

yarn add -D typescript @types/express @types/node
Copy the code

Once installed, it’s also worth noting that we don’t want to have to manually compile every code change to take effect. This is not a good experience! So we need to add a few additional dependencies:

  • Ts-node: This installation package is designed to run typescript code directly without compilation, which is necessary for local development
  • Nodemon: This installation package automatically listens and restarts development services after code changes. collocationts-nodeModules allow you to write code in a timely manner.

So both of these dependencies are required at development time without being compiled into production.

yarn add -D ts-node nodemon
Copy the code

Set up our program to run

Configure Typescript files

Create a tsconfig.json file for the typescript configuration file we will use

touch tsconfig.json
Copy the code

Now let’s add compile-related configuration parameters to the configuration file:

  • module: "commonjs"– If you’ve ever used Node, it’s essential that this is compiled into the final code as compiled code.
  • esModuleInterop: true— This option allows us to use * instead of the exported content when exporting by default.
  • target: "es6"— Unlike the front-end code, we need to control the runtime environment and ensure that the node version we use correctly recognizes ES6 syntax.
  • rootDir: "./"– Sets the root directory of the code to the current directory.
  • outDir: "./build"– Typescript code is eventually compiled into a directory of Javascript code that is executed.
  • strict: true– Allows strict type checking.

The final tsconfig.json file contains the following contents:

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "rootDir": "./",
    "outDir": "./build",
    "strict": true
  }
}
Copy the code

Configure the package.json script

There are no scripts for the package.json file yet, we need to add several scripts: the first is start to start the development mode, and the other is a command to build the environment code on the package line.

To start the development mode we need to execute nodemon index.ts, and to package the production code, we have given all the required information in tsconfig.json, so we just need to execute TSC.

Here is everything in your package.json file at the moment, but depending on when we created the project, the version number may be different.

{" dependencies ": {" express" : "^ 4.17.1"}, "devDependencies" : {" @ types/express ":" ^ 4.17.11 ", "@ types/node" : "^ 14.14.22 nodemon", ""," ^ 2.0.7 ", "ts - node" : "^ 9.1.1", "typescript" : "^ 4.1.3"}}Copy the code

Git configuration

If you use Git to manage your code, you also need to add.gitignore files to ignore the node_modules and build directories

touch .gitignore
Copy the code

Add overlooked content

node_modules
build
Copy the code

At this point, all installation is complete, and it’s probably a little more complicated than the pure Typescript free version.

Create our Express application

Let’s start building the Express application. Start by creating the main file index.ts

touch index.ts
Copy the code

And then add the case code and print “Hello world” on the page

import express from 'express';

const app = express();
const PORT = 3000;

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Copy the code

On the TERMINAL CLI, run the yarn run start command

yarn run start
Copy the code

The following output will follow:

[nodemon] 2.0.7 [nodemon] to restart at any time, enter 'rs' watching path(s): *.* [nodemon] watching extensions: ts,json [nodemon] starting `ts-node index.ts` Express with Typescript! http://localhost:3000Copy the code

We can see that the Nodemon module has listened to all file changes and started our application using the ts-Node index.ts command. We can now open our browser to http://localhost:3000 and see the desired “Hello World” output.

Functions other than “Hello World”

Our “Hello World” application is almost complete, but we need to go beyond that and add some slightly more complex features to enrich the application. General functions include:

  • Store a list of usernames and matching passwords in memory
  • Allows you to submit a POST request to create a new user
  • Allows you to submit a POST request for the user to log in and accept information returned due to incorrect authentication

Let’s realize the above functions one by one!

Save the user

First, we create a types.ts file to define the User type we use. All subsequent type definitions are written in this file.

touch types.ts
Copy the code

Then export the defined User type

export type User = { username: string; password: string };
Copy the code

All right. We will use memory to hold all users, not database or other means. Create a data directory in the root directory and create the users.ts file in it

mkdir data
touch data/users.ts
Copy the code

Now create an empty array of type User in the users.ts file

import { User } from ".. /types"; const users: User[] = [];Copy the code

Submit a new user

Next we want to submit a new user to the application. Here we will use the middleware body-parse that handles the request parameters

yarn add body-parser
Copy the code

Then import and use it in the main file

import express from 'express';
import bodyParser from 'body-parser';

const app = express();
const PORT = 3000;

app.use(bodyParser.urlencoded({ extended: false }));

app.get('/', (req, res) => {
  res.send('Hello world');
});

app.listen(PORT, () => {
  console.log(`Express with Typescript! http://localhost:${PORT}`);
});
Copy the code

Finally, we can create a POST request handler in the Users file. The handler does the following:

  • Verify that the request body contains the user name and password, and verify the validity
  • Once the submitted username password is incorrect return the status code as400Error message
  • Add a new user to the Users array
  • Return a 201 status error message

Let’s start by creating an addUser method in the data/users.ts file

import { User } from '.. /types'; const users: User[] = []; const addUser = (newUser: User) => { users.push(newUser); };Copy the code

Then go back to the index.ts file and add a route with “/users”

import express from 'express'; import bodyParser from 'body-parser'; import { addUser } from './data/users'; const app = express(); const PORT = 3000; app.use(bodyParser.urlencoded({ extended: false })); app.get('/', (req, res) => { res.send('Hello world'); }); app.post('/users', (req, res) => { const { username, password } = req.body; if (! username? .trim() || ! password? .trim()) { return res.status(400).send('Bad username or password'); } addUser({ username, password }); res.status(201).send('User created'); }); app.listen(PORT, () => { console.log(`Express with Typescript! http://localhost:${PORT}`); });Copy the code

The request body should contain both username and password, and trim() should be greater than 0. If not, the 400 status and a custom error message are returned. If this validates, the user information is added to the Users array and the 201 status is returned.

Note: Did you notice that the Users array has no way of knowing if the same user has been added twice? Let’s leave that out for now.

Let’s reopen a terminal (without shutting down the terminal where the program is running) and issue a POST request to register the interface using the curl command

curl -d "username=foo&password=bar" -X POST http://localhost:3000/users
Copy the code

You will find the following output in the terminal command line

User created
Copy the code

Then request the interface again, this time with the password as an empty string, and test if the request fails

curl -d "username=foo&password= " -X POST http://localhost:3000/users
Copy the code

We were not disappointed and successfully returned an error message

Bad username or password
Copy the code

The login function

If the status code is 200, the user has logged in successfully. If the status code is 401, the user is not authorized and the login fails.

First we add the getUser method to the data/users.ts file:

import { User } from '.. /types'; const users: User[] = []; export const addUser = (newUser: User) => { users.push(newUser); }; export const getUser = (user: User) => { return users.find( (u) => u.username === user.username && u.password === user.password ); };Copy the code

Here the getUser method will return the matching user or undefined from the Users array.

Next we’ll call the getUser method in index.ts

import express from 'express'; import bodyParser from 'body-parser'; import { addUser, getUser } from "./data/users'; const app = express(); const PORT = 3000; app.use(bodyParser.urlencoded({ extended: false })); app.get('/', (req, res) => { res.send('Hello word'); }); app.post('/users', (req, res) => { const { username, password } = req.body; if (! username? .trim() || ! password? .trim()) { return res.status(400).send('Bad username or password'); } addUser({ username, password }); res.status(201).send('User created'); }); app.post('/login', (req, res) => { const { username, password } = req.body; const found = getUser({username, password}) if (! found) { return res.status(401).send('Login failed'); } res.status(200).send('Success'); }); app.listen(PORT, () => { console.log(`Express with Typescript! http://localhost:${PORT}`); });Copy the code

Curl = curl = curl = curl = curl = curl

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/users
# User created

curl -d "username=joe&password=hard2guess" -X POST http://localhost:3000/login
# Success

curl -d "username=joe&password=wrong" -X POST http://localhost:3000/login
# Login failed
Copy the code

No problem. It all came back just as we thought it would

Explore the Express type

As you may have noticed, the deeper concepts in Express, such as custom routing, middleware, and handles, are left out of the basics. Let’s refactor it now.

User-defined route type

Maybe what we want is to create a standard routing structure like this

const route = {
  method: 'post',
  path: '/users',
  middleware: [middleware1, middleware2],
  handler: userSignup,
};
Copy the code

We need to define a Route type in the types.ts file. You also need to export the related types from the Express library: Request, Response, and NextFunction. Request represents the client’s Request data type, Response is the return value type from the server, and NextFunction is the signature of the next() method, which should be familiar to middleware users of Express.

In the types.ts file, redefine the Route type

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: any[];
  handler: any;
};
Copy the code

If you’re familiar with Express middleware, you know that a typical middleware looks like this:

function middleware(request, response, next) {
  // Do some logic with the request
  if (request.body.something === 'foo') {
    // Failed criteria, send forbidden resposne
    return response.status(403).send('Forbidden');
  }
  // Succeeded, go to the next middleware
  next();
}
Copy the code

Thus, a middleware needs to pass in three parameters, namely the Request, Response, and NextFunction types. So if we need to create a Middleware type:

import { Request, Response, NextFunction } from 'express';

type Middleware = (req: Request, res: Response, next: NextFunction) => any;
Copy the code

Express already has a type called RequestHandler, so in this case we just export from Express, and we can use type assertions if we call them individual names.

import { RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: any;
};
Copy the code

Finally, we just need to specify the type for handler. The handler is supposed to be the last step in the program execution, so we don’t need to pass in the next argument at design time, and the type is RequestHandler without the third argument.

import { Request, Response, RequestHandler as Middleware } from 'express';

export type User = { username: string; password: string };

type Method =
  | 'get'
  | 'head'
  | 'post'
  | 'put'
  | 'delete'
  | 'connect'
  | 'options'
  | 'trace'
  | 'patch';

export type Handler = (req: Request, res: Response) => any;

export type Route = {
  method: Method;
  path: string;
  middleware: Middleware[];
  handler: Handler;
};
Copy the code

Add some project structure

We need to add some structure to remove the middleware and handlers from the index.ts file

Create handler

We move some of our handlers into the Handlers directory

mkdir handlers
touch handlers/user.ts
Copy the code

So in the Handlers /user.ts file, we add the following code. The processing code related to user registration has been refactored here from the index.ts file. The important thing is that we can make sure that the Signup method satisfies our defined Handlers type

import { addUser } from '.. /data/users'; import { Handler } from '.. /types'; export const signup: Handler = (req, res) => { const { username, password } = req.body; if (! username? .trim() || ! password? .trim()) { return res.status(400).send('Bad username or password'); } addUser({ username, password }); res.status(201).send('User created'); };Copy the code

Again, we’ll create the Auth handler and add the login method

touch handlers/auth.ts
Copy the code

Add the following code

import { getUser } from '.. /data/users'; import { Handler } from '.. /types'; export const login: Handler = (req, res) => { const { username, password } = req.body; const found = getUser({ username, password }); if (! found) { return res.status(401).send('Login failed'); } res.status(200).send('Success'); };Copy the code

Finally, add a processor to our home page

touch handlers/home.ts
Copy the code

Function is very simple, just output text

import { Handler } from '.. /types'; export const home: Handler = (req, res) => { res.send('Hello world'); };Copy the code
The middleware

Without any custom middleware yet, start by creating a middleware directory

mkdir middleware
Copy the code

We’ll add a middleware that prints the client request path, called RequestLogger.ts

touch middleware/requestLogger.ts
Copy the code

Export the RequestHandler type of the middleware type to be defined from the Express library

import { RequestHandler as Middleware } from 'express';

export const requestLogger: Middleware = (req, res, next) => {
  console.log(req.path);
  next();
};
Copy the code
Create routing

Now that we have defined a new Route type and some handlers of our own, we can separate the Route definition into a file and create routes.ts in the root directory

touch routes.ts
Copy the code

Here is all the code for this file, just adding the requestLogger middleware to /login for demonstration purposes

import { login } from './handlers/auth';
import { home } from './handlers/home';
import { signup } from './handlers/user';
import { requestLogger } from './middleware/requestLogger';
import { Route } from './types';

export const routes: Route[] = [
  {
    method: 'get',
    path: '/',
    middleware: [],
    handler: home,
  },
  {
    method: 'post',
    path: '/users',
    middleware: [],
    handler: signup,
  },
  {
    method: 'post',
    path: '/login',
    middleware: [requestLogger],
    handler: login,
  },
];
Copy the code
Refactor the index.ts file

The last and most important step is to simplify the index.ts file. We replace all the route-related code with routing information declared in a forEach loop routes file. The biggest benefit of this is that the types are defined for all routes.

import express from 'express'; import bodyParser from 'body-parser'; import { routes } from './routes'; const app = express(); const PORT = 3000; app.use(bodyParser.urlencoded({ extended: false })); routes.forEach((route) => { const { method, path, middleware, handler } = route; app[method](path, ... middleware, handler); }); app.listen(PORT, () => { console.log(`Express with Typescript! http://localhost:${PORT}`); });Copy the code

This makes the structure of the code look much cleaner, which is one of the benefits of architecture. In addition, Typescript’s strong typing support ensures application stability.

The complete code

Making: github.com/fantingshen…

For more articles by the author, focus on space programming of the public