How to Set Up TypeScript with Express and Node.js

Dead Simple Chat Team

In this article we are going to learn about how to setup an express js server with typescript

here is what we are going to learn in this article

Prerequisites

Here are some of the prerequisites that you need to know and software that you need to install in your system

  • NodeJs and NPM
  • Basic Familiarity with Express and TypeScript
  • Good to have: VSCode and Postman

Setting up your Project

Here is a quick guide on how to set up your project and quickly set up and running your application

  1. Create a new project directory
mkdir sample-express-typescript-app
cd sample-express-typescript-app

2. Init the project with npm

npm init
npm init a new project

npm init will ask you several questions and you can specify the answer to these as you wish.

Once you create the project open it in VScode there you will see a package.json file open it

3. package.json file details

In the package.json file you will see a JSON object that contains all the project dependencies of your project

It will list all the node packages that your project depends on. As you install more and more packages they will be listed here

the package.json file looks something like this

{
  "name": "sample-express-typescript-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}
package.json file

Installing TypeScript and Express

Commands to install TypeScript, Express, and essential typing dependencies.

Here is how you can install typescript and expres to your project. You will see then added as dependencies in your package.json file as well

npm install typescript express 
installing typescript and express

next install the type definitions for express and node js modules

npm install --save-dev @types/express @types/node
installing type definitions for express and node modules

Setting up tsconfig.json for TypeScript configuration and explaining key options for Node.js projects.

We have installed typescript and express, we need to create the typescript configuration file. The config file specifies the root files and the compiler options.

You can generate a basic config file by running the following command

npx tsc --init

Here are some of the important options that are relevant to any node js project

  • target: You can set the ECMA script target with this flag, you can set it to ECMA 2016 or ECMA 6 script
  • module: You can configure the module with this. For Our project we are using common JS
  • rootDir: This specifies the root directory of the project. For our project we are using the src directory as our root directory
  • outDir: This specifies the directory where the compiled files will be stored. We are setting this to ./dist directory for our project
  • strict: You can set it to true for strict type check or let it to false for milder type checking
  • esModuleInterop: If you are working with ES modules in typescript then it is recommended to set it to true as it will then enable compatibility with Babel module.
  • skipLibCheck: This will cause the type script to skip checking of declaration files if checked as true.

Let us look at a common example of a generated tscofig.json file. This is a compressed version.  If you want to look at the complete generated file I have attached it at the bottom of this article

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "CommonJS",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}
sample tsconfig.json file

Configuring typescript with express

Creating a basic express server in typescript

In this section we are going to create a basic server with express and typescript.

we have already initialized a new project and installed typescript and express as dependencies and we have also created a tsconfig.json file

  1. Open the tsconfig.json file and set these properties
  • "target": "ES6": Set the ECMA script version
  • "module": "commonjs": We are using CommonJS module resolution.
  • "outDir": "./dist":We are setting the ./dist as owr complied code directory.
  • "rootDir": "./src": we are setting our root directory to be ./src
  • "strict": true: We are enabling strict type checking
  • "esModuleInterop": true: We are setting the ES Modules to compile with Common JS

Now create a src directory that would hold all your typescript files in it and create a app.ts file which will be our entry point

and a dist directory that will store the compiled files. here are all the files

./dist/app.js

This is a compiled file. You do not need to do anything here

"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 app = (0, express_1.default)();
const port = 3000;
app.get('/', (req, res) => {
    res.send('Hello World with TypeScript!');
});
app.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
});
app.js

./src/app.ts

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

const app: Application = express();
const port: number = 3000;

app.get('/', (req: Request, res: Response) => {
  res.send('Hello World with TypeScript!');
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});
app.ts

package.json

{
  "name": "sample-express-typescript-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
    "start": "node dist/app.js",
    "dev": "node dist/app.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.11.19",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "express": "^4.18.2"
  }
}
package,json

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "CommonJS",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}
tsconfig.json

to start the server run the below command

npm run start
start the server

Now, go to localhost://3000 and you can see the server running

express server with typescript

Detailed explanation of TypeScript types and interfaces to enhance Express server development.

In this section we are going to learn more about the typescript types and interfaces

  • Primitive types: The basic types are called as primitive types in typescript
  • Any and Unknown: Any allows for any type and hense should be used rarely because it bypasses the type checking of typescript. unknown is a better alternative because the typescript first tries to check the type and then perform any operation on the variables
  • Union and intersection types: Union type that is for example string | number allows the variable to be one of several types. Intersection types combine many types into one, this ensures that the variable meets all the creteria and is useful for combining interfaces
  • Custom types: These are useful for combining complex types in request and response object for express servers. the custom tyoes can be defined using the type structure

TypeScript Interfaces

  • function types: You can define function types with the help of typescript interfaces, this can be useful in creating express middleware or route handlers and reuse of type definations.
  • extending interfaces: Interfaces can extend to one or multiple interfaces. This could be useful in creating complex data structures and sharing common properties amoung those complex data structures
  • Implementing interfaces: Classes in typescript can implement interfaces, to make sure that the class adhere to specific structure. You can use these to define service classes or controllers in an express application

Typescript type system is beneficial in express server applications, because of the following reasons

  • Enhanced code quality and understandability
  • Error Reduction

Here is how you can use TypeScript types and interfaces in your express app

  1. Defining Custom interfaces for Req Bodies
  2. Using Types for Route Handlers
  3. Middleware with TypeScript

Structuring your TypeScript Express Application

Project Structure Best Practices

  1. Separation of concerns: Organize your project such that each module or component is responsible for a single aspect of the application
  2. use a modular approach for routes: Make your routes easy to manage and handle by grouping your route handlers into separate modules based on their domain or files
  3. Centralized configuration: Simplify your configuration management by keeping your app config in a centralized location. This also makes your
  4. Encapsulate database operations: Place the data modules and database acceess login in separate locations, this makes code cleaner and more maintainable
  5. Implement Middleware cognizantly: Organize middleware functions that handle errors , log request and parse request body  in a seprate directory this will help in reusing these routes and functions and thus making the code clearner and more maintainable
  6. Use TypeScript Features: Use typescript features such as type safety and other features to make code better
  7. Env Config : Store env variables in a seprate file in the root directory called the .env file this keeps your sensitive information from the GIT or any other version control system that you might be using

Here is a sample direct

my-express-app/
│
├── src/
│   ├── config/              # Application configuration and environment variables
│   │   └── index.ts
│   │
│   ├── controllers/         # Route controllers (controller layer)
│   │   ├── userController.ts
│   │   └── productController.ts
│   │
│   ├── models/              # Data models (database layer)
│   │   ├── userModel.ts
│   │   └── productModel.ts
│   │
│   ├── routes/              # Express route definitions
│   │   ├── userRoutes.ts
│   │   └── productRoutes.ts
│   │
│   ├── middleware/          # Express middlewares for request processing, auth, etc.
│   │   ├── errorHandler.ts
│   │   └── authMiddleware.ts
│   │
│   ├── interfaces/          # TypeScript interfaces and types
│   │   ├── userInterface.ts
│   │   └── productInterface.ts
│   │
│   ├── utils/               # Utility functions and helpers
│   │   └── logger.ts
│   │
│   └── app.ts               # Express app entry point
│
├── tests/                   # Test files
│   ├── userTests.ts
│   └── productTests.ts
│
├── .env                     # Environment variables
├── tsconfig.json            # TypeScript compiler configuration
└── package.json             # Project metadata and dependencies
directory layout

here we have a

Creating Modular Routes with Express and TypeScript

Step by step guide to creating Modular Routes

  1. Setting Up the project with TypeScript:

Ensure that you have initialized a typescript project and are using express with it.

  1. Creating a Router Module
  • Define a Router file:

Create a router file that will have all the routes related to a particular subject. For example if you are building user related routes you might have a file named userRoutes.ts inside the routes folder

  • Import dependencies

Import the express js and any other dependencies that you might have in your project

  1. Create and Configure the Router Module
  • Use express.router() method to create a router instance and then define route handlers using the instance
const router = express.Router();

router.get('/', (req: Request, res: Response) => {
  res.send('A group of users');
});

router.post('/', (req: Request, res: Response) => {
  const newUser = req.body;
  res.status(201).send(newUser);
});
  1. Export the router:

Make sure that you are exporting the router instance at the end of the page so that it can be imported elsewhere it is needed

export default router;
  1. Mounting Router in your express

You can use the app.use to mount the router in your application in the app.js file which is the main application file.

import express from 'express';
import userRouter from './routes/userRoutes';

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

app.use(express.json()); // you can parse JSON file with this 

// Mounting the router on the users path
app.use('/users', userRouter);

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Implementing Middleware in TypeScript

By implementing middleware in typescript, we can use the strong type safety features of typescript with our middleware and ensure code readability and maintainability

Middleware basically handles the req, res and next functions in your express js application. You can use middleware for logging, error handling and request validation tasks as well as the traditional handling of req, res and next tasks

Writing custom middleware in typescript

Middleware functions in express have access to the req Object. these functions handle req , res and next functions. The middleware function is the next function in the application req, res cycle

the next middleware function is commonly denoted with the next variable

  1. Logging Middleware

You can create a simple middleware function to log details about the req, response cycle to the console and this can help with debugging and monitoring

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

const loggerMiddleware = (req: Request, res: Response, next: NextFunction): void => {
  console.log(`${req.method} ${req.path}`);
  next();
};

In the above example the request response and nextfunction are typescript types which ensures that the arguments are correctly typed

  1. Error Handling middleware

Error handling is a bit more complex than logging, because it needs to catch errors from the previous middlewar function.

The error handling middleware takes four objects, along with the usual req, res and next it also takes an error object

  1. Request Validation Middleware

If you want to do request validation for example to ensure that in a POST request the oncoming data conforms to expected schema. You can use libraries like joi and @hapi/joi along with the typescript to provide powerful data validation along with type safety

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

const userSchema = Joi.object({
  username: Joi.string().required(),
  password: Joi.string().min(6).required()
});

const validateUser = (req: Request, res: Response, next: NextFunction): void => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    res.status(400).send(error.details[0].message);
  } else {
    next();
  }
};

In this above schema the joi is defining the expected schema class of the request body. The middleware validates the oncoming data with this schema class and if valied forwards it or returns an error

Utilizing Middleware in your application

You can use the middleware in your application like so

import express from 'express';
const app = express();

app.use(loggerMiddleware); // here we are using logger middleware globally
app.use('/api/users', validateUser, userRoutes); //here we are  using the  validation middleware for soome specific routes
app.use(errorHandler); // here we are using the Error handling middleware. It should be used the last, after all other middleware and routes

const PORT = 3000;
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

implementing middleware with typescript not only adds type safety, it also improves the code readability and maintainability.

In conclusion, this is how you can safely use typescript with your express app

DeadSimpleChat

Need Chat API for your website or app

DeadSimpleChat is an Chat API provider

  • Add Scalable Chat to your app in minutes
  • 10 Million Online Concurrent users
  • 99.999% Uptime
  • Moderation features
  • 1-1 Chat
  • Group Chat
  • Fully Customizable
  • Chat API and SDK
  • Pre-Built Chat
Metered TURN servers

Metered TURN servers

  1. Global Geo-Location targeting: Automatically directs traffic to the nearest servers, for lowest possible latency and highest quality performance. less than 50 ms latency anywhere around the world
  2. Servers in 12 Regions of the world: Toronto, Miami, San Francisco, Amsterdam, London, Frankfurt, Bangalore, Singapore,Sydney, Seoul
  3. Low Latency: less than 50 ms latency, anywhere across the world.
  4. Cost-Effective: pay-as-you-go pricing with bandwidth and volume discounts available.
  5. Easy Administration: Get usage logs, emails when accounts reach threshold limits, billing records and email and phone support.
  6. Standards Compliant: Conforms to RFCs 5389, 5769, 5780, 5766, 6062, 6156, 5245, 5768, 6336, 6544, 5928 over UDP, TCP, TLS, and DTLS.
  7. Multi‑Tenancy: Create multiple credentials and separate the usage by customer, or different apps. Get Usage logs, billing records and threshold alerts.
  8. Enterprise Reliability: 99.999% Uptime with SLA.
  9. Enterprise Scale: With no limit on concurrent traffic or total traffic. Metered TURN Servers provide Enterprise Scalability
  10. 50 GB/mo Free: Get 50 GB every month free TURN server usage with the Free Plan
  11. Runs on port 80 and 443
  12. Support TURNS + SSL to allow connections through deep packet inspection firewalls.
  13. Support STUN
  14. Supports both TCP and UDP

Generated tsconfig.json file

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./.tsbuildinfo",              /* Specify the path to .tsbuildinfo incremental compilation file. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects. */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for legacy experimental decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
    // "reactNamespace": "",                             /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */
    // "moduleDetection": "auto",                        /* Control what method is used to detect module-format JS files. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    // "rootDir": "./",                                  /* Specify the root folder within your source files. */
    // "moduleResolution": "node10",                     /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like './node_modules/@types'. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "moduleSuffixes": [],                             /* List of file name suffixes to search when resolving a module. */
    // "allowImportingTsExtensions": true,               /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
    // "resolvePackageJsonExports": true,                /* Use the package.json 'exports' field when resolving package imports. */
    // "resolvePackageJsonImports": true,                /* Use the package.json 'imports' field when resolving imports. */
    // "customConditions": [],                           /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
    // "resolveJsonModule": true,                        /* Enable importing .json files. */
    // "allowArbitraryExtensions": true,                 /* Enable importing files with any extension, provided a declaration file is present. */
    // "noResolve": true,                                /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    // "noEmit": true,                                   /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types. */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like '__extends' in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing 'const enum' declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "verbatimModuleSyntax": true,                     /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,                         /* When type checking, take into account 'null' and 'undefined'. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when 'this' is given the type 'any'. */
    // "useUnknownInCatchVariables": true,               /* Default catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read. */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Add 'undefined' to a type when accessed using an index. */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type. */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}
tsconfig.json