Here is the link to the "home page" for this blog series.

I am using Windows OS and following tools:

We are going to learn here how to:

Initialize project

Install Node.js from here.

The code is here.

To check node installation and version, run following command in your terminal

node --version

Install yarn by running

npm install -g yarn

Run

 yarn init -y

And check that package.json is created.

Add our dependencies

yarn add tsoa express body-parser
yarn add -D typescript @types/node @types/express @types/body-parser

We are using -D flag to add packages to the devDependencies in the package.json file.

Initialize tsconfig.json

yarn run tsc --init

Open VS code and check everything.

The structure should be like this

And package.json, like this

{
  "name": "FirstNodeJsApi",
  "version": "1.0.0",
  "main": "index.js",
  "repository": "https://[email protected]/sergeydotnet/NodeJs/_git/FirstNodeJsApi",
  "author": "Sergey .NET",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "tsoa": "^3.2.1"
  },
  "devDependencies": {
    "@types/body-parser": "^1.19.0",
    "@types/express": "^4.17.8",
    "@types/node": "^14.11.1",
    "typescript": "^4.0.3"
  }
}

Now add tsoa.json file and paste following code

{
  "entryFile": "src/app.ts",
  "noImplicitAdditionalProperties": "throw-on-extras",
  "controllerPathGlobs": ["src/**/*Controller.ts"],
  "spec": {
    "outputDirectory": "build",
    "specVersion": 3
  },
  "routes": {
    "routesDir": "build"
  }
}

Let's take a closer look at this configuration.

First, we specify where the entry point to our application will be. Most likely, this file will be called index.ts or app.ts. We will create this file in a second.

Afterwards, the controllerPathGlob tells tsoa where it can look for controllers, so we don't manually have to import them.

Next, we tell tsoa how strict excess property checking (to use the TypeScript term) or additionalProperty checking (to use OpenAPI terminology) should be. We can choose to "ignore" additional Properties (the OpenAPI default), remove them during validation ("silently-remove-extras"), or throw an Error back to the Client ("throw-on-extras"). I will show it later how it works by example.

Next, we set the output directory for our OpenAPI specification (OAS) and our routes.ts file, which we will talk about later.

We are setting specVersion to 3 so tsoa will generate an OpenAPI v3 specification.

Defining the first model

Let's define a User and UserDto Interfaces in src/users/user.ts and src/users/userDto.ts. Create folder src and users.

export interface User {
    id: number;
    email: string;
    name: string;
}
export interface UserDto {
    id: number;
    email: string;
    name: string;
}

Before we start defining our Controller, it's usually a good idea to create a Service that handles interaction with our Models instead of shoving all that logic into the controller layer.

import { User } from "./user";
import { UserDto } from "./userDto";

export type UserCreationParams = Pick<User, "email" | "name">;

export type UserCreationParamsDto = Pick<UserDto, "id" | "email" | "name">;

export class UsersService {
  public getUserById(id: number): UserDto {
    var user = this.getUser(id);
    var userDto = this.createUserDto(user);
    return userDto;  
  }
  public createUser(userCreationParams: UserCreationParams): User {
    return {
      id: Math.floor(Math.random() * 10000), // Random      
      ...userCreationParams,
    };
  }
  private createUserDto(user: User): UserCreationParamsDto {
    return {
      ...user
    };
  }
  private getUser(id: number): User{
    return {
      id,
      email: "[email protected]",
      name: "Jane Doe"
    };
  }
}

There are two public methods in this class getUserById and createUser. getById just have one parameter - id, expected to be a number and returns a UserDto object. getUserById gets user, typically from some persistent store. Here we just hardcoded the values in the function getUser, then we map our domain user into userDto using TypeScript Pick structure and return UserDto object. Here we are implementing Domain Driven Design (DDD). We are converting our  domain user into so called data transfer object (dto). If you are new to DDD, read the book by Eric Evans "Domain-Driven Design: Tackling Complexity in the Heart of Software". It is important to remember that our domain is like Holy Grail. No other external clients should know about it.

create is taking userCreationParams, expected to be UserCreationParams type as parameter and also returns a User object. Here is interesting point how we are creating a new User object using ... syntax and Pick structure again. And again Domain Driven Design(DDD) is used. We are converting our user so called data transfer object (dto) into our domain object User.

Defining a simple controller

import {
    Body,
    Controller,
    Get,
    Path,
    Post,
    Route
  } from "tsoa";

  import { UserDto } from "./userDto";
  import { UsersService, UserCreationParams } from "./usersService";
  
  @Route("users")
  export class UsersController extends Controller {
    @Get("{userId}")
    public async getUser(
      @Path() userId: number
    ): Promise<UserDto> {
      return new UsersService().getUserById(userId);
    }
  
    @Post()
    public async createUser(
      @Body() requestBody: UserCreationParams
    ): Promise<void> {
      this.setStatus(201); // set return status 201
      new UsersService().createUser(requestBody);
      return;
    }
  }

First, we are defining a /users/ route using the @Route() decorator above our UsersController class.

Additionally, we define 2 methods: getUser and createUser. The @Get() decorator in combination with our base route /users/ will tell tsoa to invoke this method for every GET request to /users/, where {userId} is a template.

Because we are using @Path() decorator with an userId of type number will tell tsoa to reject passing i.e. a string here.

The @Post decorator in combination with our base route /users/ will tell tsoa to invoke this method for every POST request to /users/ with json object like this

{    
    "email": "[email protected]",
    "name": "Jane Doe"
}

And, again this json object, called dto will be converted in the domain object User in the usersService. Remember we are normally not defining id when we are creating objects. There are of course some exceptions. Remember also to set response code. Response code when we creating something is 201. Here is the list of all response codes.

And add "experimentalDecorators": true to the tsconfig.json file in the compilerOptions section to get rid of warnings in the VS code.

My tsconifg.json looks like this

Creating the express server

Create an app.ts file under src folder and paste following code

import express from "express";
import bodyParser from "body-parser";
import { RegisterRoutes } from "../build/routes";

export const app = express();

// Use body parser to read sent json payloads
app.use(
  bodyParser.urlencoded({
    extended: true,
  })
);
app.use(bodyParser.json());

RegisterRoutes(app);

Then create server.ts file and paste following code

import { app } from "./app";

const port = process.env.PORT || 3000;

app.listen(port, () =>
  console.log(`Example app listening at http://localhost:${port}`)
);

 At this point you may have noticed that TypeScript will not find the RegisterRoutes import from build/routes. That's because we haven't asked tsoa to create that yet. Let's do that now. First create folder build like this

And run

yarn run tsoa routes

Check that routes.ts is generated.

Now it is time to compile TypeScript

yarn run tsc --outDir build

and start the server

node build/src/server.js

The output should be like this

Testing using Postman

Open Postman or just browser and try out

Postman output Get Request

For Post Request

You may want to add these scripts to your package.json.  And update main as well

"main": "build/src/server.js",
"scripts": {
    "build": "tsoa spec-and-routes && tsc --outdir build",
    "start": "node build/src/server.js"
  }

To use these scripts

yarn build
yarn start

Congratulations! We've just created our first Node.js API.

Here is the link to the "home page" for this blog series.

Let me know in the comments if you experienced any troubles or if you have any feedbacks. Don't forget to subscribe to get news from Sergey .NET directly to your mailbox.

If you like my post image, you can free download it from here.