TypeScript teaches some manners to write JavaScript code. A well written JavaScript code (TypeScript) grabs many attention in the party; so why not we simply include TypeScript to write Nodes Express APIs? Great idea! And both NodeJS and Express play very well with this decision.
Without wasting much time will jump start with this one. Lets us first globally install TypeScript if not already. Following command will help us to do so.
npm i -g typescript
Create a project folder where you want to start painting NodeJS Express with TypeScript. We have created a folder named tsc-express-app. Now let us initialize typescript configuration using the following command inside your project folder.
tsc --init
This will generate a configuration file named as tsconfig.json. Update the file and add following properties to the compilerOptions property if not present already.
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
}
}
Here we are specifying our rootDir as "src" folder which will hold the TypeScript files and the JavaScript files which will be generated after building the project will be stored in "outDir" folder.
Now initialize the project using npm initialize command as follows inside your project folder.
npm --init
After initializing the project with default information we will start adding packages to our project.
Add following dependencies and dev-dependencies to your project.
npm i express
npm i -D typescript
npm i -D ts-node-dev
npm i -D @types/node
npm i -D @types/express
The -D above is for dev-dependencies. The dependencies in package.json will be as follows after running the above command.
{
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.4",
"@types/node": "^13.9.8",
"ts-node-dev": "^1.0.0-pre.60",
"typescript": "^3.8.3"
}
}
Here we have installed express as dependencies while the other packages as dev-dependencies, the reason behind this is that the dev-dependencies packages are only required for development and not when he project got build or deployed. So this includes typescript too.
The @types/* packages are the packages those are responsible for providing meta information for packages. There are many @types/* packages are available for most of the packages. So in order to avoid TypeScript errors when running the project we must add these @types/* pacakges to our dev-dependencies. In our case we are using only express and node so we added @types/node and @types/node.
The process of working with TypeScript involves building the project and then executing it. This will be done with the help of 2 set of commands shown below:
For building the project (transpiling from TypeScript to JavaScript):
tsc -p .
For running the server:
node dist/server
In order to speed up our development time we have added ts-node-dev package. It does transpile our TypeScript code to JavaScript and place them into dist folder and execute our project immediately when we do any TypeScript code changes. It simply provide a watch feature to our TypeScript code so we can see changes on fly. The command for this package is as follows:
ts-node-dev --respawn --transpile-only ./src/server.ts
For simplfying things we have created our own set of NPM commands to provide some ease in doing common operations like build, start and watch.
"scripts": {
"start": "node dist/server",
"build": "tsc -p .",
"watch": "ts-node-dev --respawn --transpile-only ./src/server.ts"
}
Here the start will be used to run the JavaScript file which can be obtained after executing the build command. Or we can avoid this 2 steps by direclty executing the watch command which will run the sever and watch for any TypeScript code changes in the background. The combination of such commands are shown below:
npm run build
npm start
// or
npm run watch
Below shows the folder structure of the project.
tsc-express-app
|-- dist
|-- node_modules
|-- src
|-- data
|-- employees.json
|-- models
|-- employee.ts
|-- routes
|-- employees.ts
|-- server.ts
|-- package.json
|-- tsconfig.json
The dist folder will be generated once we run the npm run build command. The node_modules holds the dependencies. The src folder contains the TypeScript files. The data folder holds the static employee data in .json format, which will act as a data source for current example. The server.ts file will start the server while the routes/employees.ts file defines the routes for employee REST API end points. The model here defines the data type Employee for each object present in employees.json file.
Will first create the Employee model named models/employee.ts file using TypeScript interface as shown below.
export interface Employee {
Id: number;
Name: string;
Job: string;
Department: string;
Code: string;
}
Create the API end points for employees inside the routes folder. The routes/employees.ts file is shown below.
import express, { Router, Request, Response } from 'express';
import { Employee } from '../models/employee';
import employeesJson from './../data/employees.json';
const router: Router = express.Router();
const employees = employeesJson as Employee[];
// GET: api/employees
router.get('/', async (req: Request, res: Response) => {
try {
res.json(employees.sort((a, b) => b.Id - a.Id));
} catch (error) {
res.status(500).json(error);
}
});
// GET: api/employees/:id
router.get('/:id', async (req: Request, res: Response) => {
try {
const employee = employees.find(i => i.Id == +req.params.id);
if (employee) {
res.json(employee);
} else {
res.status(404).json({
message: 'Record not found'
});
}
} catch (error) {
res.status(500).json(error);
}
});
// POST: api/employees/
router.post('/', async (req: Request, res: Response) => {
try {
const employee = req.body as Employee;
employee.Id = Math.max(...employees.map(i => i.Id)) + 1;
employees.push(employee);
res.json(employee);
} catch (error) {
res.status(500).json(error);
}
});
// PUT: api/employees/:id
router.put('/:id', async (req: Request, res: Response) => {
try {
const index = employees.findIndex(i => i.Id === +req.params.id);
const employee = employees[index];
if (employee) {
employees[index] = { ...employee, ...(req.body as Employee) };
res.json(employees[index]);
} else {
res.status(404).json({
message: 'Record not found'
});
}
} catch (error) {
res.status(500).json(error);
}
});
// DELETE: api/employees/:id
router.delete('/:id', async (req: Request, res: Response) => {
try {
const index = employees.findIndex(i => i.Id === +req.params.id);
const employee = employees[index];
if (index !== -1) {
employees.splice(index, 1);
res.json(employee);
} else {
res.status(404).json({
message: 'Record not found'
});
}
} catch (error) {
res.status(500).json(error);
}
});
module.exports = router;
Let us talk about the imports first, here we are importing the defaul express object along with classes like Router, Request and Response. The meta definition for these classes are visible due to the dev-dependency @types/express. Next we will include Employee interface from the model folder. Finally we will import the employees.json data and for importing the .json file like this we have added "resolveJsonModule": true property in the tsconfig.json file; otherwise it will show error for importing .json file like below.
import express, { Router, Request, Response } from 'express';
import { Employee } from '../models/employee';
import employeesJson from './../data/employees.json';
We then created router object using express.Router() method and obtained employees array from employees.json file. We have specfied the type as Employee[] which will indicate the type of employees constant.
const router: Router = express.Router();
const employees = employeesJson as Employee[];
We have then added end points for REST methods GET, POST, PUT and DELETE through which we can perform CRUD operations. Let us finally create the server to define entry point for the application. The server.ts file is shown below.
import express, { Application } from 'express'
const app: Application = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use('/api/employees', require('./routes/employees'));
app.listen(PORT, () => {
console.log(`Server started running on ${PORT}`);
});
We have defined port 3000 and also our routes for the employee API end points.
As discussed earlier we can run the project using either build and start command or watch. We can try both.
npm run build
npm start
For build and start we need to again and again run this 2 commands when ever we change our code. So for this reason we can go with the watch command which will auto detect change and build-run immediately for us.
npm run watch
NOTE: Once we are done with our development in order to deploy the project to production we must have to supply the dist folder which is generated after building the project (npm run build).
Additionally we can hit the end points using the api-spec.http file present in the project root folder. If we open the project inside Visual Studio Code and installed the extension REST Client then we can hit those end points from api-spec.http file.
A more detailed explanation about NodeJS, ExpressJS with TypeScript is explained in the article (link: Building a Node.js/TypeScript REST API, Part 1: Express.js); this reading highlights the healthy typescriptive way of registering routes and maintaining the project structure.
December 31, 2020
October 19, 2020
March 02, 2022