Editorβs note: This article was updated by David Omotayo on 26 September 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:
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:
Check out the GitHub repository for the source code; the main branch has the TypeScript project, and the JavaScript branch has the JavaScript version.
package.json
fileStart 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" }, ... }
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.
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" }, ... }
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 outputmodule
: Facilitates the utilization of a module manager in the compiled JavaScript code. CommonJS is supported and is a standard in Node.jsstrict
: Toggles strict type-checking protocolsesModuleInterop
: Enables the compilation of ES6 modules to CommonJS modulesskipLibCheck
: When set to true
, bypasses the type checking of default library declaration filesforceConsistentCasingInFileNames
: When set to true
, enforces case-sensitive file namingOne 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
.
.ts
extensionTransforming 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.
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.
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.
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.
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:
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:
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 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.
Hey there, want to help make our blog better?
Join LogRocketβs Content Advisory Board. Youβll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]
40 Replies to "How to set up TypeScript with Node.js and Express"
What is your terminal theme? The package and nodejs icon are really cool additions
Hey Jay, the theme is called Snazzy.
why compile?
So that the typescript files are converted to js as node cannot execute ts files
short and sweet
You saved my life π
What about that ‘media’ command early on in the article ?
It says :
media server
cd server/
Hey Gunnar,
Thanks for catching up that. It should mkdir — to create the server directory.
What’s the best command setup for deploying and running something like this in production? There are now compiled JS files in build/, but other files (e.g., package.json, static HTML, etc.) are all in their original locations. And, we haven’t created a script in package.json to run the built app.
okayy,, then how can i use other plugin to work with my express typescript. am i should install plugin that support in typescript or not ?
You would need something like WebPack to handle the build requirements.
Choose any middleware you need, but make sure DefinitelyTyped has the corresponding Typescript @types/* mappings available. Most popular NodeJS middleware packages are supported in the DefinitelyTyped project.
great article
I had to run npm install ts-node -g to install ts-node globally so that nodemon found it
Same here
Fix `https` to `http` in console.log() statement:
“””
console.log(`[server]: Server is running at https://localhost:${port}`);
“””
Hey, if you are running localhost, you do not require “https” until you want to intentionally use HTTPS.
You have a typo: `dotnev`
Can you point me towards which typo?
“outDir” should be “./dist”
In the article above, the “outDir” is referring to the config option. The “./dist” is the value.
Are you referring to something specific? Can you please point me to that?
instead of ‘/dist’, should be ‘./dist’ — otherwise it goes to machine root
Understood. Thanks for catching that Kelvin!
when rename `index.js` to `index.ts`, `package.json` `mian` field must change to `index.ts` too.
You just saved me. Thank you!
The `main` poperty in the `package.json` indicates the entry point and it should point out to Javascript compiled output. So it should be “main”:”dist/index.js” as we are using typescript here, if it was just just Javascript we can just use the index.js.
Thanks. Ok just so everyone is 100% clear. package.json should be changed to:
“main”: “dist/index.js”,
Excellent tutorial! One minor note is that the protocol should not be `https` since express is not secure by default, e.g, this line should be changed to just `http` — `console.log(`β‘οΈ[server]: Server is running at http://localhost:${port}`)`
This is a minor but crucial fix. Thanks for pointing it out!
had to remove “module”: “commonjs” from tsconfig.js as the error “ReferenceError: exports is not defined in ES module scope”
I had to remove `”type”: “module”,` from the `package.json` file to fix the issue you mention @Dey
thanks bro
anyone has a sample github repo for this? please include an example repo so we know where to place the files at. im guessing all the files should be in an dist folder?
https://github.com/D-Lite/express-ts
This is a starter template following the article above
Thanks for creating a starter template for this!
Nice. Antidote to the framework madness!
Thanks, this is really helpful!
This is an excellent article that helped me complete what I sought out to do. I have one suggestion, where there is a suggestion to update nodemon.json with this script:
{
“watch”: [“src”],
“ext”: “ts”,
“exec”: “concurrently \”npx tsc –watch\” \”ts-node src/index.ts\””
},
I would have found it helpful to add a comment about installing concurrently. It was suggested earlier in the article but not in the same general vicinity of the nodemon.json change.
i should install as dev dependency right?
Life saver !