Node.js with TSOA and Authentication - (Part 2) Basic Setup

In this article, I'll walk you through the step-by-step process of creating a Node.js project that implements the basic framework for calling a REST API endpoint. Let's start with the basic setup!

4 minutes

We're starting this series with an empty Node.js project. To get a better understanding, please check out Part 1: Node.js with TSOA and Authentication – The Objective .

The goal of this article is to run a Node.js backend that can call an endpoint with built-in authentication. This will generate the following error, which we'll address later in the series:

Error: [AuthMiddleware] not in use - received "jwt" and scopes: ADMIN  
    at expressAuthentication 
// + StackTrace

Installing the necessary packages

The first step is installing some NPM packages.

npm install express tsoa
npm install -D @types/express vitest

The TsConfig

We'll set up a TsConfig. We'll write tests later in the series, but the configuration is already prepared. Appropriate compiler flags are also set. Finally, we'll add the absolute import values for the src folder.

{  
  "include": [  
    "./src/**/*",  
    "./tests/**/*"  
  ],  
  "exclude": [  
    "./node_modules"  
  ],  
  "compilerOptions": {  
    "target": "es2022",  
    "lib": [  
      "es2022"  
    ],  
    "experimentalDecorators": true,  
    "emitDecoratorMetadata": true,  
    "module": "es2022",  
    "moduleResolution": "node",  
    "baseUrl": ".",  
    "paths": {  
      "@/*": [  
        "src/*"  
      ],  
      "tsoa": [  
        "node_modules/tsoa/dist"  
      ],  
      "tsoa/": [  
        "node_modules/tsoa/dist/"  
      ]  
    },  
    "types": [  
      "vitest/globals"  
    ],  
    "resolveJsonModule": true,  
    "allowJs": false,  
    "outDir": "dist",  
    "isolatedModules": true,  
    "allowSyntheticDefaultImports": true,  
    "esModuleInterop": true,  
    "forceConsistentCasingInFileNames": true,  
    "strict": true,  
    "noImplicitAny": true,  
    "strictNullChecks": true,  
    "strictFunctionTypes": true,  
    "strictBindCallApply": true,  
    "strictPropertyInitialization": true,  
    "noImplicitThis": true,  
    "alwaysStrict": true,  
    "noUnusedParameters": true,  
    "noImplicitReturns": true,  
    "noFallthroughCasesInSwitch": true,  
    "skipLibCheck": true  
  }  
}

Scripts for the Package.json

We add three scripts to the package.json file:

  • start:dev to start the application and ensure that TSOA builds the routes beforehand. This also serves as a watch command to accelerate development and feedback.
  • ts to be called in the terminal to check if TypeScript is being compiled (and for the Git hook, if we want to use one).
  • tsoa:gen as a shortcut for the terminal call and as part of the start:dev call to rebuild the routes.
"scripts": {  
  "start:dev": "npm run tsoa:gen && npx tsx --watch ./src/index.ts",  
  "ts": "tsc --noEmit",  
  "tsoa:gen": "tsoa spec-and-routes"  
}

TSOA Configuration

The tsoa.json file looks like this – the authenticationModule is essential. I'll explain that in a moment.

{  
  "entryFile": "src/index.ts",  
  "noImplicitAdditionalProperties": "throw-on-extras",  
  "controllerPathGlobs": [  
    "src/**/*Controller.ts"  
  ],  
  "spec": {  
    "outputDirectory": "dist",  
    "specVersion": 3  
  },  
  "routes": {  
    "routesDir": "src",  
    "authenticationModule": "./src/services/AuthMiddleware.ts"  
  },  
  "compilerOptions": {  
    "baseUrl": ".",  
    "paths": {  
      "@/*": [  
        "src/*"  
      ]  
    }  
  }  
}

The TSOA Authentication Module

In the TSOA configuration, under the “routes” key, we defined the following: "authenticationModule": "./src/services/AuthMiddleware.ts". This specifies the location of the Express Middleware implementation.

On the TSOA side, we need to provide an export for “expressAuthentication” in the file named “authenticationModule”. The function signature includes:

  • The Express request we want to secure.
  • The security name, as we write it in our @Scope declaration. This allows us to define different authentication methods – here, we'll limit ourselves to JWT.
  • The scopes, as we write them as an array in our @Scope declaration. In our code, we will define an enum to centralise the scope names.
import { Request } from 'express'  

export type AuthResultType = {   
  userId: string  
  scopes: string[]  
}  

const expressAuthentication = (  
  _request: Request,  
  securityName: string,  
  requiredScopes?: string[],  
): Promise<AuthResultType> => {  
  return Promise.reject(new Error(`[AuthMiddleware] not in use - received "${securityName}" and scopes: ${requiredScopes}`))  
}  

export {expressAuthentication}

In the code above, we generate the error we're aiming to reproduce today. Next, we'll look at the controller for the endpoint.

A First Controller

The controller provides the endpoint, which, using npm run tsoa:gen, generates the file routes.ts, which we then integrate into the Express application.

import { Controller } from '@tsoa/runtime'  
import { Get, Route, Security } from 'tsoa'  

@Route('entities')  
export class EntityController extends Controller {  
  @Get()  
  @Security('jwt', ['ADMIN'])  
  public async getEntities (): Promise<string[]> {  
    return ['entity1', 'entity2']  
  }  
}

The initial Express application

Finally, the Express application that integrates the routes and listens on a port is still missing.

import express from 'express'  
import { RegisterRoutes } from '@/routes'  

const PORT = process.env.PORT || 4000  

const app = express()  
RegisterRoutes(app)  
app.listen(PORT, () => {  
  console.log(`Server is listening on port ${PORT}`)  
})

Conclusion

We've made good progress. We can now run the prepared configuration with npm run start:dev and test it with curl "http://localhost:4000/entities". The desired error will be displayed.

As a next step, we'll create a user account to log in.

call to action background image

Subscribe to my newsletter

Receive once a month news from the areas of software development and communication peppered with book and link recommendations.