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. collocation
ts-node
Modules 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 as
400
Error 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