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`