😁set up TypeScript Node.js and Express (build,start,dev,outDir)

https://blog.logrocket.com/how-to-set-up-node-typescript-express/

Chú ý: Để chạy lệnh import express from "express";

app.ts

import express from "express";
const app = express();
const port = 3000;
app.get('/', (req:any, res:any) => {
  res.send('Express + TypeScript Server');
});
app.listen(port, () => {
  console.log(`[server]: Server is running at http://localhost:${port}`);
});

Tôi cấu hình như sau

tsconfig.json

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",    
    "esModuleInterop": true, 
    "forceConsistentCasingInFileNames": true,          
    "strict": true,    
    "skipLibCheck": true
  }
}

package.json

{
  "name": "app",
  "version": "1.0.0",
  "main": "dist/app.js",
  "scripts": {
    "test": "npx ts-node app.ts",
    "build": "npx tsc",
    "dev": "nodemon app.ts",
    "start": "npx tsc && node dist/app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.1",
    "@types/typescript": "^2.0.0",
    "express": "^4.21.1",
    "nodemon": "^3.1.7",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.2"
  }
}

Part 1

package.json

{
  "name": "blog",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "dotenv": "^16.4.6",
    "express": "^4.21.1"
  }
}

src\index.js

const express = require('express');
const dotenv = require('dotenv');
dotenv.config();
const app = express();
const port = process.env.PORT;
app.get('/', (req, res) => {
  res.send('Express + TypeScript Server');
});
app.listen(port, () => {
  console.log(`[server]: Server is running at http://localhost:${port}`);
});

.env

# Add all of the environmental variables here instead of 
# embedding them directly in the app and utilize them 
# with the `DotEnv` package.
PORT=3000

Part 2

package.json

{
  "name": "blog",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "dotenv": "^16.4.6",
    "express": "^4.21.1"
  },
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.1",
    "concurrently": "^9.1.0",
    "nodemon": "^3.1.7",
    "ts-node": "^10.9.2",
    "typescript": "^5.7.2"
  }
}

src\index.ts

import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
dotenv.config();
const app: Express = express();
const port = process.env.PORT || 3000;
app.get("/", (req: Request, res: Response) => {
  res.send("Express + TypeScript Server website");
});
app.listen(port, () => {
  console.log(`[server]: Server is running at website http://localhost:${port}`);
});

Editor’s note: This article was updated by David Omotayo on 7 November 2024 to cover setting up path aliases using tsconfig.json.

Creating a server with TypeScript using Node.js and Express is a good alternative to using JavaScript because it makes it easier to manage complex applications and helps when you need to collaborate with a distributed team of developers.

TypeScript offers benefits like:

  • Improved code strength and clarity when static typing

  • Enhanced collaboration and project scalability

  • Advanced tooling

  • IDE support

  • Broad compatibility

All of these benefits make TypeScript a great choice for a smoother development experience, especially in evolving projects.

In this article, we’ll explore a beginner-friendly way to configure TypeScript in an Express app, as well as gain an understanding of the fundamental constraints that accompany it. To follow along, you should have:

  • Node.js ≥ v18.x installed in your local development environment

  • Access to a package manager like npm, pnpm, or Yarn

  • Basic familiarity with Node.js and Express

Check out the GitHub repository for the source code; the main branch has the TypeScript project, and the JavaScript branch has the JavaScript version.

Creating a package.json file

Start by creating a new directory in your local development environment, and within it, use npm’s initializer command to create a package.json file. If you use a package manager other than npm, consider adhering to the init command provided by that specific package manager:

mkdir ts-node-express
cd ts-node-express/
npm init -y

When initializing a package.json file in this manner, the --yes or -y flag utilizes the default settings configured by npm, bypassing the repeated questions asking for project details. The resulting package.json file might look similar to the one shown in the following image:

Because the entry point of our application will be src/index.js, which we will address in the upcoming sections, you should update the main field in the package.json file from index.js to src/index.js:

{
  "name": "ts-node-express",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  ...
}

Creating a minimal server with Express

After initializing the package.json file, add the Express and DotEnv packages to the project. In the terminal window, run the following command, where npm i is an alias to npm install:

npm i express dotenv

The DotEnv package is used to read environment variables from a .env file. Instead of hardcoding environment-specific variables directly into the app, include them all in this file and utilize the DotEnv package to manage them.

For instance, to specify the port number for your server, create a file named .env in the root of the project directory. Inside this file, define an environment variable for PORT and set its value to 3000. Consider adding more environment-specific variables in this file in the future as needed:

# Add all of the environmental variables here instead of 
# embedding them directly in the app and utilize them 
# with the `DotEnv` package.

PORT=3000

Then, create a directory called src at the project’s root to organize our application source files. Add a new file named index.js to it and populate it with the following code, including the previously defined environmental variable:

// src/index.js
const express = require('express');
const dotenv = require('dotenv');

dotenv.config();

const app = express();
const port = process.env.PORT;

app.get('/', (req, res) => {
  res.send('Express + TypeScript Server');
});

app.listen(port, () => {
  console.log(`[server]: Server is running at http://localhost:${port}`);
});

The above code covers the essential steps for setting up a minimal Express server using plain JavaScript. For a more detailed explanation, refer to the documented version of this snippet.

To start the server, execute the command node src/index.js in the terminal. This will execute the code that we just added to the index.js file and should start a new server, as illustrated below:

The Express server is now up and running, offering a foundational setup for development with Express on Node.js. Next, let’s enhance it by incorporating TypeScript in the next section.

Installing TypeScript

We will begin by installing TypeScript as a development dependency. Additionally, we’ll install the @types declaration packages for Express and Node.js, which offer type definitions in the form of declaration files.

Declaration files, typically denoted with the .d.ts extension, serve as predefined modules that outline the structure of JavaScript values or the types present for the TypeScript compiler. These declaration files are available for libraries originally written in JavaScript, not TypeScript.

The DefinitelyTyped GitHub repository maintains the TypeScript type definitions for direct use in Node.js and other JavaScript projects, sparing you the effort of defining these types from scratch. To incorporate types or declaration files for a specific library or module, seek packages starting with the @types namespace.

Launch the terminal and install the packages described above using the following command:

npm i -D typescript @types/express @types/node

The -D, or --dev, flag directs the package manager to install these libraries as development dependencies.

Installing these packages will add a new devDependencies object to the package.json file, featuring version details for each package, as shown below:

{
  ...
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.7.4",
    "typescript": "^5.6.2"
  },
  ...
}

Generating the TypeScript configuration file: tsconfig.json

Every TypeScript project utilizes a configuration file to manage various project settings. The tsconfig.json file, which serves as the TypeScript configuration file, outlines these default options and offers the flexibility to modify or customize compiler settings to suit your needs.

The tsconfig.json file is usually placed at the project’s root. To generate this file, use the following tsc command, initiating the TypeScript Compiler:

npx tsc --init

Once you execute this command, you’ll notice the tsconfig.json file is created at the root of your project directory. This file contains the default compiler options, as depicted in the image below:

Upon opening the tsconfig.json file, you’ll notice several other commented-out compiler options. Among all of these options, compilerOptions is a mandatory field that must be specified. Here’s a summary of all the default options that belong inside the compilerOptions field:

  • target: Enables the specification of the target JavaScript version that the compiler will output

  • module: Facilitates the utilization of a module manager in the compiled JavaScript code. CommonJS is supported and is a standard in Node.js

  • strict: Toggles strict type-checking protocols

  • esModuleInterop: Enables the compilation of ES6 modules to CommonJS modules

  • skipLibCheck: When set to true, bypasses the type checking of default library declaration files

  • forceConsistentCasingInFileNames: When set to true, enforces case-sensitive file naming

One crucial option you will need to enable is outDir, which determines the destination directory for the compiled output. Locate this option in the tsconfig.json file and uncomment it.

By default, the value of this option is set to the project’s root. Change it to dist, as shown below:

{
  "compilerOptions": {
    ...
    "outDir": "./dist"
    ...
  }
}

While there are probably other configuration options you can add to the TypeScript compiler, the options above are basic specifications that can help you get started.

You should now update the main field in the package.json file to dist/index.js because the TypeScript code will compile from the src directory to dist.



Creating an Express server with a .ts extension

Transforming our JavaScript Express server code into TypeScript isn’t as complicated as it may seem. Begin by renaming the file from index.js in the src directory to index.ts. The .ts extension indicates a TypeScript file, and it will be compiled into JavaScript when we build the application later.

Now, open the index.ts file and add the following modifications to make it TypeScript-compatible:

// src/index.ts
import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";

dotenv.config();

const app: Express = express();
const port = process.env.PORT || 3000;

app.get("/", (req: Request, res: Response) => {
  res.send("Express + TypeScript Server");
});

app.listen(port, () => {
  console.log(`[server]: Server is running at http://localhost:${port}`);
});

No additional changes are made to the code except for including some TypeScript types. Refer to the documented version of the above code for a more detailed overview of what’s happening.

Now, if you attempt to execute the index.ts file using Node, similar to what we did with our index.js file, you will encounter an error:

This is because Node doesn’t inherently support the direct execution of TypeScript files. The next section discusses running TypeScript files in the terminal using a Node package.

Running TypeScript in Node with ts-node

As previously discussed, executing a TypeScript file in Node is not supported by default. However, we can overcome this limitation by leveraging ts-node, a TypeScript execution environment for Node. Let’s first use ts-node with npx without installing it as a dependency and observe the output:

npx ts-node src/index.ts

As shown below, our index.ts file executed successfully, and the server started running as expected:

The main advantage of using ts-node is that it eliminates the extra step of code transpilation and allows you to work with TypeScript code directly in a Node.js environment. It also comes in handy when working with standalone TypeScript files in the Node terminal.

Watching file changes

To enhance the development workflow for Node.js projects, I often use nodemon, a utility library that automatically restarts a Node-based application upon detecting file changes in the specified directories.

Another useful package you might consider is concurrently, which facilitates the execution of multiple commands, such as nodemon, npx, tsc, etc., allowing you to combine different functionalities. In this section, we’ll explore how concurrently can be used alongside nodemon to improve workflow.

Because nodemon doesn’t work with TypSscript files out of the box, we will also install ts-node as a development dependency. This ensures nodemon automatically picks up ts-node to hot reload the Node server when changes are made to TypeScript files, streamlining the development process.

Execute the following command to integrate nodemon and ts-node as development dependencies:

npm i -D nodemon ts-node concurrently

After installing these dev dependencies, update the scripts in the package.json file as follows:

{
  "scripts": {
    "build": "npx tsc",
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts"
  }
}

Referring to the added script modifications above, the build command compiles the code into JavaScript and saves it in the dist directory using the TypeScript Compiler (tsc). The dev command is designed to run the Express server in development mode with the help of nodemon and ts-node.

Finally, return to the terminal window and execute npm run dev to initiate the development server. It should show something like this:

There are no errors, confirming that the server is running successfully. As nodemon has identified changes, let’s try to edit the message sent from res.send() while concurrently monitoring the terminal for any detected changes by nodemon:

Taking an extra step for a more refined setup, you may consider a nodemon.json file in the project root, which serves as a configuration file for nodemon. This file lets you specify directories and extensions to watch and define commands to execute, while nodemon manages the reloading of the application upon changes:

{
  "watch": ["src"],
  "ext": "ts",
  "exec": "concurrently "npx tsc --watch" "ts-node src/index.ts""
}

It’s crucial to note that combining the TypeScript Compiler command in watch mode with ts-node or any other command, as demonstrated above in the nodemon configuration or with the nodemon command itself, may result in a loss of logging information.

This is due to both nodemon and TSC concurrently monitoring changes and potentially competing to display their respective logs on the screen.

Building or transpiling the TypeScript files

In a TypeScript project, transpiling or building involves the TypeScript Compiler (TSC) interpreting the tsconfig.json file to determine how to convert TypeScript files into valid JavaScript.

To compile the code, you must execute the command npm run build. A new dist directory is created in the project root after successfully executing this command for the first time. Within this directory, you will find the compiled versions of our TypeScript files in the form of valid JavaScript. This compiled JavaScript is essentially what is used in the production environment:

'use strict';
var __importDefault =
  (this && this.__importDefault) ||
  function (mod) {
    return mod && mod.__esModule ? mod : { default: mod };
  };
Object.defineProperty(exports, '__esModule', { value: true });
const express_1 = __importDefault(require('express'));
const dotenv_1 = __importDefault(require('dotenv'));
dotenv_1.default.config();
const app = (0, express_1.default)();
const port = process.env.PORT;
app.get('/', (req, res) => {
  res.send('Express + TypeScript Server is running.');
});
app.listen(port, () => {
  console.log(`⚡️[server]: Server is running at http://localhost:${port}`);
});

If you designate any other directory as the value for the outDir field in the tsconfig.json file, that specified directory would be reflected here instead of dist.

To improve this process further, set up TypeScript for reliability with strict type checking and configurations that adapt to your needs. Make the most of the tsconfig.json file by specifying the best suitable production settings for your project. Improve performance with code splitting by utilizing tools like webpack for efficiency and shrink file sizes with tools like Terser.

As the project expands, ensure code stability through automated testing with tools like Jest and streamline the workflow from development to production with CI/CD pipelines.

Setting up a path alias using ts.config

Another option you might want to consider configuring in the tsconfig.json file is the path and baseUrl options. The path option allows you to define aliases, while the baseUrl specifies the base directory for module resolution.

With these options, you can simplify import statements by defining custom paths or shortcuts for directories in your projects. This is particularly useful when managing large, complex codebases where long, complex relative paths can be replaced with short, easy-to-read ones:

// Without path aliases 
import UserService from '../../../services/user/UserService'; 

// With path aliases 
import UserService from '@services/UserService';

Setting up a path alias is fairly straightforward, all you have to do is add or uncomment the baseUrl and path options in the tsconfig file and add the following modifications:

{
  "compilerOptions": {
    ...
    "baseUrl": "./",                                  
    "paths": {
      "@services/*": ["src/services/*"],
      "@utils/*": ["src/utils/*"]
    },
    "outDir": "./dist"
    ...
  }
}

In this example, I’ve set up two paths: services and utils, which contain the files userServices.ts and logger.ts, respectively.

In the excerpt above, we set the root directory as the base directory from which relative paths in import statements are resolved by using "./". You can set this to any directory depending on your project structure. In the path option, we define aliases that point to the specified directories, where @services points to src/services, and @utils points to src/utils.

Now, instead of using long relative paths to import userServices.ts and logger.ts in src/index.ts:

import { getUser } from "./services/userService";
import { log } from "./utils/logger";

We can use the aliases specified in the paths option like so:

import { getUser } from "@services/userService";
import { log } from "@utils/logger";

However, if you try to start the development server, you’ll get the following error:

This happens because Node.js doesn’t natively understand TypeScript path aliases, so the paths in tsconfig.json aren’t recognized during runtime. To resolve those aliases, you need to install a runtime package like tsconfig-paths.

Execute the following command in your terminal to install tsconfig-paths:

npm install tsconfig-paths --save

Then, modify your dev script in the package.json file like this:

  "scripts": {
    ...
    "dev": "nodemon -r tsconfig-paths/register src/index.ts",
    ...
  },

The -r tsconfig-paths/register src/index.ts flag tells Node.js to use tsconfig-paths to resolve aliases defined in the tsconfig file. After saving the code and running the dev script again, the server should spin up without issues:

Note: If you’re using nodemon in watch mode, you need to update the exec script and add the -r tsconfig-paths/register src/index.ts flag in the nodemon.json file to avoid encountering the same error as before.

{
  ...
  "exec": "concurrently \"npx tsc --watch\" \"ts-node -r tsconfig-paths/register src/index.ts\""
}

Next, we need to make sure the transpiler correctly resolves the TypeScript imports to relative paths in the JavaScript file in dist/index.js. For example, after transpiling, the userServices and log imports should look like this in the compiled JavaScript:

const userService_1 = require("./services/userService");
const logger_1 = require("./utils/logger");

This happens because the transpiler doesn’t automatically resolve path aliases, so they stay unchanged in the JavaScript output file:

This will cause the same error as before when you run the start script, as the JavaScript runtime doesn’t recognize the path aliases.

To fix this, we’ll use a third-party tool like tsc-alias to replace path aliases with relative paths after TypeScript is compiled. Go back to your terminal and install the package with the command below:

npm install --save-dev tsc-alias

Once the installation is complete, open the package.json file and update the build script as follows:

  "scripts": {
    "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json",
    ...
  },

This command ensures the TypeScript compiler follows the configuration provided in your tsconfig.json file and rewrites the path aliases in the generated JavaScript files.

With these changes, the next time you run the build script, the transpiler will correctly resolve the TypeScript aliases to relative paths, and the error should cease:

Conclusion

In this guide, we explored how to set up TypeScript with Node.js and Express, focusing on configuring key elements for a smooth development experience. We created a server, configuring ts-node, and using nodemon for hot reloading to streamline the workflow.

Additionally, we discussed setting up TypeScript path aliases using tsconfig.json to simplify complex imports, a useful feature when working with large codebases. Finally, we covered how to use tsconfig-paths and tsc-alias to ensure path aliases are properly resolved during runtime. These tools — ts-node, nodemon, and TypeScript path aliases — can significantly enhance your Node.js development workflow.

Using TypeScript has its benefits, but it does come with a bit of a learning curve. You have to carefully analyze whether using TypeScript in your Node.js and Express backend projects is beneficial or not, which may depend on the requirements of your project.

Feel free to fork the GitHub repo for this project, point out any issues you noticed, and ask questions. Happy coding!

LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

Last updated