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
- Defining the first model
- Defining a simple controller
- Creating the express server
- Testing using Postman
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.