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

Setup

Create the project's folder:

sh
mkdir node-app cd node-app

Generate a default package.json file with yarn:

sh
yarn init -y

Install Typescript:

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

Install Express, Jest, Pino and some middleware:

sh
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:

json
{ "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:

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

Then, open package.json and add:

json
,"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:

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

Create the controller in hello.controller.ts:

js
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:

js
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:

js
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:

js
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:

js
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`