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!
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:devto start the application and ensure that TSOA builds the routes beforehand. This also serves as a watch command to accelerate development and feedback.tsto be called in the terminal to check if TypeScript is being compiled (and for the Git hook, if we want to use one).tsoa:genas a shortcut for the terminal call and as part of thestart:devcall 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
@Scopedeclaration. 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
@Scopedeclaration. 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.