Create a simple server with Node.js, Express and Typescript

Setup

Create the project's folder:

mkdir node-app
cd node-app

Generate a default package.json file with yarn:

yarn init -y

Install Typescript:

yarn add -D typescript tslint ts-node-dev ts-node @types/node  

Install Express, Jest, Pino and some middleware:

yarn add express cors pino pino-pretty 

yarn add -D @types/express @types/cors @types/jest @types/supertest jest ts-jest@next supertest 

Create a file named tsconfig.json and paste:

{
  "ts-node": {
    "transpileOnly": true
  },
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "pretty": true,
    "sourceMap": true,
    "target": "ES2020",
    "outDir": "./dist",
    "removeComments": true,
    "esModuleInterop": true,
    "baseUrl": "./",
    "strict": true,
    "lib": [
      "ES2020"
    ],
    "typeRoots": [
      "node_modules/@types"
    ]
  }
}

Create a file named jest.config.js with:

module.exports = {
  verbose: false,
  testMatch: [
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
  ],
  transform: {
      "^.+\\.(ts|tsx)$": "ts-jest"
  },
}

Then, open package.json and add:

,"scripts": {
    "prod": "tsc",
    "dev": "tsnd --respawn src/main.ts",
    "lint": "tslint 'src/**/*.ts'",
    "audit": "npm audit fix",
    "test": "jest"
  }

Hello World

Create the Hello Module

Create the following files & folders structure:

...
/src/
  modules/
    hello/
      hello.service.ts
      hello.controller.ts
      hello.test.ts
      hello.routes.ts
...

Add the code that will say hello in hello.service.ts:

class HelloService {
  public static sayHello(name: string): string {
    return `Hello ${name}`
  }
}

export default HelloService

Create the controller in hello.controller.ts:

import { NextFunction, Request, Response } from 'express';
import HelloService from './hello.service';

export class HelloController {

  public hello(req: Request, res: Response, next: NextFunction): void {
    res.json({ message: "Hello World!" })
  }

  public getHello(req: Request, res: Response, next: NextFunction): void {
    const name = req.params.name
    const message = HelloService.sayHello(name)
    res.json({ message })
  }

  public sayHelloTo(req: Request, res: Response, next: NextFunction): void {
    const to = req.body.sayHelloTo
    const message = HelloService.sayHello(to)
    res.json({ message })
  }
}

Then add the routes in hello.routes.ts:

import express from 'express';
import { HelloController } from './hello.controller';

export class HelloRoutes {

  private readonly _router = express.Router();
  private controller = new HelloController();

  constructor() {
    this.routesSetup()
  }

  get router(): express.Router {
    return this._router
  }

  private routesSetup() {
    this._router.get("/hello", this.controller.hello)
    this._router.get("/helloTo/:name", this.controller.getHello)

    this._router.post("/hello", this.controller.sayHelloTo)
  }
}

Add a test in hello.test.ts:

import app from '../../main';
import request from 'supertest';
import HelloService from './hello.service';

describe("demo test", () => {
  it("should be equal", () => {
    expect(1).toBe(1)
  })
})

const testTarget = app // OR: 'http://localhost:3000'; 

describe('Test REST Endpoint', () => {
  it('Request /hello should return Hello World!', async () => {
    const result = await request(testTarget).get('/hello').send();
    expect(result.status).toBe(200);
    expect(result.body).toMatchObject({ message: "Hello World!" })
  });

  it('Request /helloTo/test should return Hello test!', async () => {
    const result = await request(testTarget).get('/helloTo/test').send();
    expect(result.status).toBe(200);
    expect(result.body).toMatchObject({ message: "Hello test" })
  });

  it('POST Request /hello should return Hello test2!', async () => {
    const result = await request(testTarget).post('/hello').send({ sayHelloTo: 'test2' });
    expect(result.status).toBe(200);
    expect(result.body).toMatchObject({ message: "Hello test2" })
  });
});

///

describe("Test Hello Service", () => {
  it("should say Hello World", () => {
    const result = HelloService.sayHello('World')
    expect(result).toBe('Hello World')
  })
})

afterAll(done => {
  done();
});

Main app

Create a custom error message in src/custom-error.ts:

export enum HttpStatusCode {
  OK = 200,
  BAD_REQUEST = 400,
  NOT_FOUND = 404,
  INTERNAL_SERVER = 500,
}

class AppError extends Error {
  public readonly statusCode: HttpStatusCode
  public readonly message: string
  public readonly type: string

  constructor(message: string, statusCode: HttpStatusCode, description: string = '') {
    super(description)

    this.message = message
    this.statusCode = statusCode
    this.type = description

    Error.captureStackTrace(this)
  }
}

export default AppError

And the server's entry point in src/main.ts:

import cors from 'cors';
import express, { Application, NextFunction, Request, Response } from 'express';
import AppError, { HttpStatusCode } from './custom-error';
import pino from 'pino'

import { HelloRoutes } from "./modules/hello/hello.routes"

export const Logger = pino({
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      levelFirst: true,
      translateTime: "yyyy-mm-dd, h:MM:ss TT",
    }
  }
});

const app: Application = express();

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

//
// 
// Setup routes

const routes = [
  new HelloRoutes()
]

routes.forEach(item => app.use('/', item.router))

//
// 
// return error for all routes that are not matched 

app.all('*', (req: Request, res: Response, next: NextFunction) => {
  next(new AppError(`Can't find ${req.originalUrl}`, HttpStatusCode.NOT_FOUND))
});

// 
// 
// Error Handling Middleware

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  let statusCode = 500

  if (err instanceof AppError) {
    statusCode = err.statusCode
  }

  Logger.error(err.message)

  res.status(statusCode).json({
    type: 'error',
    message: err.message
  });
});


const server = app.listen(3000, '0.0.0.0');

// for tests
export default server

Run tests with yarn test

Start the server with yarn dev then open a browser and visit http://localhost:3000/hello`