How to Setup a TypeScript + NodeJs Server (2023)
Tags: typescript, node, jest, eslint
Published: 2023-02-18 07:31:28

With new releases and tools, setting up a node server has become super simple and until NodeJs ships with typescript built-in, adding typescript is an essential need.

I will show you the *SIMPLEST* setup you can have to kick off your next node project confidently. For simplicity, you can customize it with the things you need to complete your project.

## Project Directory Setup
Let's start by setting you your project directory

```bash
mkdir YOUR_PROJECT_NAME
cd YOUR_PROJECT_NAME

git init # start your git project

npm init -y # initialize npm with defaults

mkdir src # create the source directory for all the code
```

## Setting up Typescript
Let's start with a couple of installs…

```bash
npm install -D typescript @types/node
```

The `@types/node` pretty much sets up types for the entire Node itself.
We can now create the `ts-config.json` file by running:

```bash
npx tsc --init
```

This will add a `ts-config.json` file to the root of the project with all defaults including commented-out ones. We need to have the following configurations

```json
{
  "compilerOptions": {
     "target": "es2016",
     "module": "commonjs",
     "rootDir": "./",
     "resolveJsonModule": true, /* in case you are importing JSON files */
     "outDir": "./build",
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
     "strict": true,
     "noImplicitAny": true,
     "skipLibCheck": true,
  }
}
```

Feel free to uncomment a few other options to fit your project. This should be good to start.

## Create a simple server

To test if everything is working fine, let's add a simple server code.
Create a file `src/index.ts` and add the following code:

```typescript
// src/index.ts

import http from "http";

export const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(
    JSON.stringify({
      data: "It Works!",
    })
  );
});

server.listen(3000, () => {
  console.log("Server running on http://localhost:3000/");
});
```

This is a simple HTTP Node server listening on port 3000. It should be enough to test our setup.

Now let's try to build this by adding a build script to our package.json file.

```json
"scripts": {
  "build": "tsc"
}
```

Now if you run `npm run build` you should see a build directory added to the project root folder with a `src` directory and the `package.json` file. The server code should look something like so:

```javascript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.server = void 0;
const http_1 = __importDefault(require("http"));
exports.server = http_1.default.createServer((req, res) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
        data: "It Works!",
    }));
});
exports.server.listen(3000, () => {
    console.log("Server running on http://localhost:3000/");
});
```

This confirms our build works!

## Set up how to run the server

We have to think about it in terms of the final build. Because we know the final build will be in the build directory and it will be in javascript, we can set the start script like so:

```json
"scripts": {
  "build": "tsc",
  "start": "node src"
}
```

The start will not work if we try to run it from our root directory because our source code is in typescript. For that, we need to do a couple more setups.

```bash
npm install -D nodemon rimraf npm-run-all ts-node
```

With those installed let's improve our build by adding a `clean` script and make the `build` script clean before the new build.

```json
"scripts": {
  "clean": "rimraf ./build",
  "build": "npm run clean && tsc",
  "start": "node src",
}
```

With that, let's add the scripts to run the project locally:

```json
"scripts": {
  "clean": "rimraf ./build",
  "build": "npm run clean && tsc",
  "start": "node src",
  "local": "ts-node src",
  "local:watch": "nodemon src -e ts,json --exec 'npm run local'",
}
```

The local script uses `ts-node` to run our project instead of `node` command. This is pretty much a node but in typescript.
But `local` command only runs the server once and will not detect your changes. That's why the `local:watch` is needed so we can run the server and watch for changes when you are working.

It uses `nodemon` to run the project in `src` directory watching for extensions (`-e`) `ts` and `json` so whenever there are changes to execute ( `--exec` ) the `npm run local` script command.

When developing, simply run `npm run local:watch` .

![server running on port 3000](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27um10jq9yyhx2ell10i.png)

*Note: I am using a JSON Viewer extension in Chrome*

## Test Setup

I do not recommend going without a test. There have been great things happening on the testing side of things with the release of a built-in test runner in Node which will make things simpler.

However, I still use jest and the following setup is easy to swap to something else if you prefer.

Start with the following installs:

```bash
npm i -D supertest @types/supertest jest @types/jest ts-jest
```

We will be using `supertest` to test our server and the rest is just `jest` , its types ( `@types/jest`) and a jest typescript version ( `ts-jest` ) to run things.

Now you can add the `jest.config.js` file to the project root folder with the following code:

```javascript
module.exports = {
  transform: {
    '^.+\\.ts?$': 'ts-jest',
  },
  testEnvironment: 'node',
  testRegex: './src/.*\\.(test|spec)?\\.(js|ts)$',
  moduleFileExtensions: ['ts', 'js', 'json'],
  roots: ['<rootDir>/src'],
};
```

The setup is super simple, feel free to add more extensions as you need but overall, that's all you need to start with jest and typescript.
With that, we can add the test script:

```json
"scripts": {
  "clean": "rimraf ./build",
  "build": "npm run clean && tsc",
  "start": "node src",
  "local": "ts-node src",
  "local:watch": "nodemon src -e ts,json --exec 'npm run local'",
  "test": "jest"
}
```

Now let's test our server by adding the `index.test.ts` file inside the src directory with the following code:

```typescript
import supertest from "supertest";
import { server } from "./index";

describe("Server", function () {
  const request = supertest.agent(server);

  afterAll((done) => {
    server.close(done);
  });

  it("should get /", async () => {
    const res = await request.get("/");
    
    expect(res.status).toBe(200)
    expect(res.body).toEqual({"data": "It Works!"})
  });
});
```

This is simply checking if when we make a `GET` request to our basic server, we get what we set it to return.


![success test run](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2q51tm1rat61l91ezsp8.png)

One thing we need to do now is to exclude test files ( `*.test.ts`) from our build by adding them to the exclude option in the `ts-config.json` file.

```json
{
  "compilerOptions": {
     "target": "es2016",
     "module": "commonjs",
     "rootDir": "./",
     "resolveJsonModule": true, /* in case you are importing JSON files */
     "outDir": "./build",
     "esModuleInterop": true,
     "forceConsistentCasingInFileNames": true,
     "strict": true,
     "noImplicitAny": true,
     "skipLibCheck": true,
  },
  "include": [
     "src",
     "package.json"
  ],
  "exclude": [
     "src/**/*.test.ts"
  ]
}
```

We also want to include the `package.json` so the `npm start` can work when we deploy the build directory somewhere like **Netlify**, **AWS ElasticBeanStalk**, **Heroku**, etc.

Use the exclude and include list to add or remove final production artifacts.

## ESLint and Prettier Setup

Linting and auto-formatting our code makes it consistent and easier to read. It also catches things we often don't pay attention to by improving the quality of our code even more.

Start by installing few things:

```bash
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint prettier eslint-config-prettier
```

Now create the `.eslintrc` file with the following code:
```json
{
  "extends": [
    "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint"
  ],
  "root": true
}
```

We can now create the `lint` , `format` and `format:check` scripts like so:
```json
"scripts": {
  "clean": "rimraf ./build",
  "build": "npm-run-all lint format clean && tsc",
  "start": "node src",
  "local": "ts-node src",
  "local:watch": "nodemon src -e ts,json --exec 'npm run local'",
  "lint": "eslint src",
  "format": "npx prettier --write src",
  "format:check": "npx prettier --check src",
  "test": "jest"
}
```

Note that the `build` script was changed to run the `lint` script before the cleaning and build.

You should configure your IDE or code editor to use the eslint configuration to auto-check and format things when you make code saves to get the best of this setup. Otherwise, manually run these commands on your own.

This is how it looks for JetBrains IDEs like IntelliJ and WebStorm:

![WebStorm eslint setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3tqmf4gceii0185n978l.png)

## Final touches
We need the `.gitignore` file to make sure we don't commit things we don't have to. It should look like this
```bash
# IntelliJ project files
.idea
*.iml
out
gen

# VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets

# Local History for Visual Studio Code
.history/

# Built Visual Studio Code Extensions
*.vsix

# Folders
dist
build
node_modules
```

## Take Away
This setup is all you need to start. Depending on your project you may need additional things.

Check all the code in the [following template repo](https://github.com/beforesemicolon/node-typescript-project-template). Let me know if you have any questions or need further assistance with something else.

**YouTube Channel**: [Before Semicolon](https://www.youtube.com/channel/UCrU33aw1k9BqTIq2yKXrmBw)
**Website**: [beforesemicolon.com](https://beforesemicolon.com/)