DEV Community

Roberto Luna
Roberto Luna

Posted on

Implementing Organization‑Scoped Multi‑Tenancy in a NestJS Monorepo – Fixing the `Req` Import & Guard Issue

Implementing Organization‑Scoped Multi‑Tenancy in a NestJS Monorepo – Fixing the Req Import & Guard Issue

TL;DR: I updated the AdministradorasController to correctly import Req and adjust the UseGuards decorator, which stopped NestJS from throwing “Cannot read property ‘user’ of undefined” when resolving the tenant from the request. The change unlocked proper organization scoping across all API routes.


The Problem

Our monorepo’s API layer (apps/api) is being refactored to support multi‑tenancy based on an organization_id header. The AdministradorasController is one of the first endpoints we scoped. After adding a custom OrganizationGuard, every request to /administradoras started failing with:

Error: Cannot read property 'user' of undefined
    at OrganizationGuard.canActivate (src/common/guards/organization.guard.ts:42:23)
    at Reflector.get (node_modules/@nestjs/core/reflector/reflector.js:45:15)
Enter fullscreen mode Exit fullscreen mode

The guard expects the request object (Req) to be injected, but Nest was passing undefined. The stack trace pointed to the controller’s decorator usage, not the guard itself.


What I Tried First

My initial attempt was to add the guard at the module level:

@Module({
  controllers: [AdministradorasController],
  providers: [OrganizationGuard],
})
export class AdministradorasModule {}
Enter fullscreen mode Exit fullscreen mode

I also tried importing Req inside the guard directly:

import { Request } from 'express';
...
canActivate(context: ExecutionContext) {
  const req = context.switchToHttp().getRequest<Request>();
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Both approaches compiled, but the runtime error persisted. The guard never received the request because the controller’s method signature didn’t expose it, and the global guard registration didn’t bind the request context correctly.


The Implementation

The root cause turned out to be a missing Req import in the controller and an incorrectly applied UseGuards decorator. The diff from commit 14da996a shows the exact changes:

Before (broken)

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AdministradorasService } from './administradoras.service';

@Controller('administradoras')
export class AdministradorasController {
  constructor(private readonly service: AdministradorasService) {}

  @Get()
  @UseGuards(OrganizationGuard) // ← Guard applied, but no Req in method
  findAll() {
    return this.service.findAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

After (fixed)

import {
  Controller,
  Get,
  UseGuards,
  Req,               // <-- added import
} from '@nestjs/common';
import { AdministradorasService } from './administradoras.service';
import { OrganizationGuard } from '../../common/guards/organization.guard';

@Controller('administradoras')
export class AdministradorasController {
  constructor(private readonly service: AdministradorasService) {}

  @Get()
  @UseGuards(OrganizationGuard)
  findAll(@Req() request: Request) { // <-- inject request
    // The guard already validated organization_id; we just forward it.
    const orgId = request.headers['organization_id'];
    return this.service.findAllByOrg(orgId);
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points in the diff

File Change
apps/api/src/administradoras/administradoras.controller.ts Added Req to the import list and injected it into the findAll method.
Same file Imported OrganizationGuard from the shared guards folder for clarity.
Same file Updated the service call to findAllByOrg(orgId) to demonstrate tenant‑aware data fetching.

Why this works: Nest’s @Req() decorator pulls the underlying Express Request object from the execution context. By explicitly declaring it in the method signature, the guard’s canActivate receives a fully populated request, allowing it to read organization_id without hitting undefined.

Guard Implementation (unchanged but worth showing)

@Injectable()
export class OrganizationGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const req = context.switchToHttp().getRequest<Request>();
    const orgId = req.headers['organization_id'];
    if (!orgId) {
      throw new ForbiddenException('Missing organization_id');
    }
    // Attach orgId to request for downstream handlers
    (req as any).organizationId = orgId;
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Additional Files Updated

The content‑automation scripts that generate weekly posts also needed a tiny bump to reference the new controller path, but those changes are purely markdown updates and don’t affect the runtime.


Key Takeaway

When building multi‑tenancy with NestJS, always inject the request object (@Req()) into any route that a guard depends on. A guard can only access what the execution context provides; if the controller method doesn’t expose the request, the guard sees undefined. This tiny import change prevented a cascade of runtime errors and kept the tenant resolution logic clean and testable.


What's Next

  1. Service Layer Refactor: Move tenant filtering logic into a reusable TenantRepository so every service can call repo.findByOrg(orgId, ...).
  2. Integration Tests: Add e2e tests that send requests with and without organization_id to verify the guard’s behavior.
  3. Docker Compose Update: Extend the docker-compose.yml to inject a mock ORGANIZATION_ID env var for local dev, ensuring developers can test tenant‑scoped routes without hitting the API gateway.

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México


Tags: #vibecoding #buildinpublic #nestjs #typescript #multitenancy #docker



Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-09-09

#playadev #buildinpublic

Top comments (0)