Open Source
thunderid-nestjs

Published: October 26, 2026 · by Srinu Desetti (creator & maintainer)

thunderid-nestjs: A Community NestJS SDK for ThunderID

thunderid-nestjs brings ThunderID (opens in a new tab) authentication into NestJS the NestJS way: a global module you register once with forRoot(), an injectable ThunderIDService for sign-in/sign-out flows, a ThunderIDGuard that blocks unauthenticated requests, and a @CurrentUser() decorator that resolves the authenticated user. It is built on top of @thunderid/express (opens in a new tab) and works with NestJS applications running on the default Express platform. I created and maintain this package as a community SDK, ported from thunder-id/javascript-sdks#26 (opens in a new tab).

Packagethunderid-nestjs (opens in a new tab) v0.1.0
LicenseApache-2.0
Runtime dependencies1 (@thunderid/express)
Peer dependency@nestjs/common ≥ 10
Node.js≥ 20.12
TypeScript declarationsIncluded (ESM + CJS builds)
Sourcegithub.com/webdevelopersrinu/thunderid-nestjs (opens in a new tab)

The Problem: Hand-Wiring an Identity Provider Into Every Service

ThunderID ships first-class Express support through @thunderid/express — but a NestJS codebase doesn't want raw Express middleware scattered through main.ts. NestJS has its own idioms for exactly this shape of problem: modules for configuration, providers for injectable clients, guards for route protection, param decorators for request-scoped values.

Without a dedicated module, every NestJS service that talks to ThunderID ends up re-implementing the same three pieces by hand:

  1. Client construction and configuration. Each service builds its own ThunderID client from env vars, in its own place, in its own way.
  2. A homegrown auth guard. Every team writes its own CanActivate that checks the session and attaches the user — slightly differently each time.
  3. A homegrown user decorator. Every controller reaches into req to pull the authenticated user back out.

That is three copies of the same glue per service, and three places for them to drift apart. thunderid-nestjs packages the glue once: register the module at the root, and the service, guard, and decorator are available everywhere.

the-whole-surface.ts
// The entire NestJS-facing surface:
import {
  ThunderIDModule,   // global module — forRoot(config) once at the root
  ThunderIDService,  // injectable: signIn, signOut, isSignedIn, getUser…
  ThunderIDGuard,    // route guard — 401 for unauthenticated requests
  CurrentUser,       // param decorator — the authenticated User
} from 'thunderid-nestjs'

// Everything from @thunderid/express (and transitively
// @thunderid/node / @thunderid/javascript) is re-exported too.

Setup: One Module Registration

Install the package alongside cookie-parser — ThunderID sessions ride on cookies, so the Express cookie parser must run before any ThunderID code sees the request:

npm install thunderid-nestjs cookie-parser

Register cookie-parser on the app, then the ThunderID module at the root:

main.ts
import cookieParser from 'cookie-parser';

const app = await NestFactory.create(AppModule);
app.use(cookieParser());
app.module.ts
import {ThunderIDModule} from 'thunderid-nestjs';

@Module({
  imports: [
    ThunderIDModule.forRoot({
      clientId: process.env.THUNDERID_CLIENT_ID,
      baseUrl: process.env.THUNDERID_BASE_URL,
    }),
  ],
})
export class AppModule {}

The module is global — ThunderIDService and ThunderIDGuard are injectable everywhere, with no re-imports in feature modules. The forRoot() config (ThunderIDNestConfig) has the same shape as the underlying Node SDK config, so nothing new to learn if you've configured ThunderID elsewhere.

Sign-In and Sign-Out Routes

ThunderIDService drives the browser-facing auth flow. signIn(req, res) is two flows in one call: on the first visit it redirects the response to the ThunderID sign-in page; on the callback it exchanges the result for tokens and returns them.

auth.controller.ts
import {Request, Response} from 'express';
import {ThunderIDService} from 'thunderid-nestjs';

@Controller()
export class AuthController {
  constructor(private readonly thunderID: ThunderIDService) {}

  @Get('login')
  async login(@Req() req: Request, @Res() res: Response) {
    const tokens = await this.thunderID.signIn(req, res);
    if (tokens.accessToken || tokens.idToken) {
      res.redirect('/');
    }
    // If no tokens were returned, signIn has already redirected the
    // response to the ThunderID sign-in page.
  }

  @Get('logout')
  async logout(@Req() req: Request, @Res() res: Response) {
    if (this.thunderID.isSignOutSuccess(req)) {
      res.redirect('/');
      return;
    }
    await this.thunderID.signOut(req, res);
  }
}

Sign-out mirrors it: signOut(req, res) starts the flow, and isSignOutSuccess(req) detects the return leg so the controller can send the user home.

Protecting Routes: Guard + Decorator

This is where the NestJS idioms pay off. Protecting a route is one @UseGuards() line, and reading the authenticated user is one parameter:

profile.controller.ts
import {CurrentUser, ThunderIDGuard, User} from 'thunderid-nestjs';

@Controller()
export class ProfileController {
  @UseGuards(ThunderIDGuard)
  @Get('profile')
  profile(@CurrentUser() user: User) {
    return user;
  }
}

ThunderIDGuard returns 401 Unauthorized for requests without a valid session. @CurrentUser() resolves the authenticated User attached by the guard — so controller signatures stay typed and declarative, with no req.user as any reaching into the raw request.

API Surface

ExportDescription
ThunderIDModule.forRoot(config)Global module; accepts ThunderIDNestConfig (same shape as the Node SDK config)
ThunderIDServicesignIn(req, res), signOut(req, res), isSignOutSuccess(req), isSignedIn(req), getUser(req), getClient()
ThunderIDGuardRoute guard that blocks unauthenticated requests
@CurrentUser()Param decorator resolving the authenticated User (requires ThunderIDGuard)

And one escape hatch: everything from @thunderid/express — and transitively @thunderid/node / @thunderid/javascript — is re-exported, so lower-level primitives are reachable from the same import when the NestJS-shaped surface isn't enough.

Design Notes — Decisions I Kept and Why

Build on @thunderid/express, not from scratch

NestJS's default HTTP platform is Express, and ThunderID already maintains a tested Express SDK. This package deliberately stays a thin adapter: @thunderid/express owns the protocol work (sessions, cookies, token exchange), and thunderid-nestjs owns only the translation into NestJS idioms — module, provider, guard, decorator. When the upstream SDK improves, this package inherits it through a version bump instead of re-implementing it.

A global module, registered once

Authentication is cross-cutting by nature — nearly every feature module eventually needs the guard or the service. Making ThunderIDModule global means forRoot() runs exactly once at the application root, and no feature module ever imports it again. One configuration site, zero drift.

getClient() as the escape hatch

The service exposes the high-level flows most apps need, plus getClient() for everything else. Combined with the full @thunderid/express re-export, the NestJS-shaped surface never becomes a ceiling: anything the underlying SDK can do remains reachable without a second dependency.

Dual ESM/CJS builds

The package ships native ESM with a CommonJS build alongside (dist/index.js + dist/cjs/index.cjs, both typed), so it loads cleanly in both module systems a NestJS project might use today.

This is a community-maintained SDK — I created and maintain thunderid-nestjs independently. It is not an official ThunderID product; it is ported from thunder-id/javascript-sdks#26 (opens in a new tab) and built on the official @thunderid/express SDK.

What's Next?

v0.1.0 covers the full module–service–guard–decorator loop on the default Express platform. Contributions are welcome — see CONTRIBUTING.md (opens in a new tab) in the repository.

← Back to Open Source overview


Frequently Asked Questions

Do I need @thunderid/express installed separately?

No — it is the package's single runtime dependency and comes along automatically. Better: everything it exports (and transitively @thunderid/node / @thunderid/javascript) is re-exported from thunderid-nestjs, so one import path covers both the NestJS-shaped surface and the lower-level primitives.

Why is cookie-parser a separate install step?

ThunderID sessions ride on cookies, and NestJS on the Express platform does not parse cookies by default. Registering cookie-parser in main.ts before any ThunderID code runs is the one piece of app-level wiring the module cannot do for you — it has to run as Express middleware on the app instance itself.

Does it work with the Fastify platform?

No — the package is built on @thunderid/express and targets NestJS applications running on the default Express platform. The service methods take Express Request/Response objects directly.

Is this an official ThunderID package?

No. It is community-maintained — created and maintained by Srinu Desetti, ported from the upstream thunder-id/javascript-sdks#26 pull request. It builds on the official @thunderid/express SDK, so the protocol-level behavior comes from ThunderID's own code; only the NestJS adaptation layer lives here.