Building an Express.js Application with TypeScript

Express.js is a popular web application framework for Node.js that simplifies the process of building robust and scalable web applications based on the middleware architecture. It provides a minimalistic and flexible set of features for creating web servers and handling HTTP requests and responses.

There are several tools that provide a boilerplate app but rarely an Express server with TypeScript, today we are going to start one from scratch.

  • Create a package.json file

  • Create a minimal server with Express

  • Installing TypeScript

  • Final adjustments

1. Package.json

We start creating a new folder where the whole server is going to be. And we execute yarn init to create the package.json . You should end up with something like this:

{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}

Copied to clipboard!

From this point we can add new packages to the repo.

2. Test a simple script

Now we can try to create a index.js and insert a simple console.log('hello world'), to test it we first need to add nodemon with yarn add --dev nodemon and then add to the package.json a new script and display the log when we execute it.

"scripts": {
  "dev": "nodemon index.js"
}

Copied to clipboard!

3. Create a minimal express server

Let's add express yarn add express and add the following code at index.js

const express = require("express");

const app = express();
const port = 8080;

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

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

Copied to clipboard!

We can add .env and .env.example files to start using environment variables. Create the files and add PORT=8080 to them. If we want to read it we need to add a package yarn add dotenv and a few lines to the express server, the file should look like:

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}`);
});

Copied to clipboard!


4. Install TypeScript

Time to start TypeScript, we'll need to install it along with all the types for Express and Node.js with yarn add --dev typescript ts-node @types/express @types/node @types/dotenv. Once it's installed we need to create the configuration file for TypeScript, run the following yarn tsc --init and after that will appear a tsconfig.json in the directory.

Now we should have the file with the default config.

  • target: Set the JavaScript language version for emitted JavaScript and include compatible library declarations.

  • module: Specify what module code is generated.

  • esModuleInterop: Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility.

  • forceConsistentCasingInFileNames: Ensure that casing is correct in imports.

When TypeScript complies the code the outDir is going to define where is going to be the final code, let's set it to /dist folder. We also can change several options, change the following options in your file.

{
	"ts-node": {
    "files": true
  },
  "compilerOptions": {
    "outDir": "./dist",
		"moduleResolution": "node",
    "rootDirs": [
      "node_modules",
      "test"
    ],
    "typeRoots": [
      "node_modules/@types",
      "types"
    ],
		"noUnusedLocals": true,
    "noUnusedParameters": true,
  },
	"include": ["src/**/*", "test/**/*", "types/**/*"],
  "exclude": ["node_modules", ".vscode"]
}

Copied to clipboard!

Now, let's change the Express server to TypeScript. Start by renaming index.js to index.ts

import express, { Express, Request, Response } from 'express';
import dotenv from 'dotenv';

dotenv.config();

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

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

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

Copied to clipboard!

In addition we need to add ts-node package with yarn add --dev ts-node , modify the development script to "dev": "nodemon ./src/index.ts" and add src folder to move the index.ts inside.

The idea is to set up a project to have not also the server but also have tests and types, so create 2 new folders:

  • types: this folder will contain types that are shared throughout our entire application.

  • __test__: as per jest convention; you can use other folder names. Here we’ll create the test that cover the entire application. Unit tests are going to go in deeper, more specific folders.

5. Final adjustments

We need to prepare the server for production and real world scenarios, let's add some other packages that will help to achieve it.

Set up ESLint & Prettier

First let's add esLint with yarn add --dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint and create the .eslintrc.json with this config.

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "overrides": [],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint", "prettier"],
  "rules": {}
}

Copied to clipboard!

We'll need a .prettierrc file with the following config and the package to avoid conflicts with esLint yarn add --dev prettier eslint-plugin-prettier

{ 
	"trailingComma": "es5", 
	"tabWidth": 2, 
	"semi": false, 
	"singleQuote": true 
} 

Copied to clipboard!

Finally modify the scripts to build the app and add EsLint configuration to the package.json. We also need to change the main and source directories.

"main": "dist/main.js",
"source": "src/index.js",
"scripts": {
  "start": "node dist/index.js",
  "build": "tsc",
  "dev": "nodemon src/index.ts",
  "lint": "eslint ./src/**"
}

Copied to clipboard!

Make sure you ignore important or build files with .gitignore

A .gitignore file and add .env.example because the .env would be ignored.

.DS_Store
*node_modules*
dist
*dist
.env

Copied to clipboard!

Wrapping up

That's it for today, you can use it as boilerplate and forget about configuration in your own projects, a task that always takes time and it's not really interesting so you can focus on the fun part, coding.]

There are still some considerations on how to type effectively an express application, but that’s for another post. Some ideas if you want to start researching for yourself. Or tune in to our social media so you know when we release more tutorials like this in the future.

  • How to extend an Express Request with custom data from middlewares.

  • Where to store our custom types in an Express application when using TypeScript.