Skip to content

RequestContext

Overview

The useRequestContext utility provides type-safe access to the current HTTP request context, including the authenticated user, role information, authorization helpers, and response formatting functions. It wraps the IGNIS Framework's useRequestContextInfra() with application-specific types and BANA conventions.

Source: packages/core/src/utilities/request.utility.ts (89 lines)

Import

typescript
import { useRequestContext } from '@nx/core';
// or
import { useRequestContext } from '@nx/core/utilities';

Return Type

The useRequestContext() function returns an object with the following properties:

PropertyTypeDescription
contextContextThe raw Hono request context object
currentUserIJWTTokenPayloadFull decoded JWT payload
userIdstringShortcut for currentUser.userId
rolesstring[]Array of role identifier strings extracted from currentUser.roles
isAlwaysAllowedbooleantrue if user has SUPER_ADMIN or ADMIN role
useCountDatabooleanWhether to wrap array responses in { data, count } format
normalizeCountableData<T>(opts) => T[] | { data: T[], count: number }Normalize list results with Content-Range headers
formatResponse<T>(result, statusCode?) => ResponseFormat a single-item response
formatArrayResponse<T>(result, statusCode?) => ResponseFormat an array response

Basic Usage

typescript
import { useRequestContext } from '@nx/core';

@service()
export class ProductService {
  async createProduct(data: CreateProductDto): Promise<TProduct> {
    const { userId, roles, isAlwaysAllowed } = useRequestContext();

    // Check authorization
    if (!isAlwaysAllowed && !roles.includes('merchant-admin')) {
      throw new ForbiddenError('Insufficient permissions');
    }

    // Use userId for audit trail
    return this.productRepository.create({
      data: {
        ...data,
        createdBy: userId,
      },
    });
  }
}

Context Properties

currentUser

The full decoded JWT token payload:

typescript
interface IJWTTokenPayload {
  userId: string;
  email?: string;
  username?: string;
  roles: Array<{
    id: string;
    name: string;
    identifier: string;
  }>;
  organizerId?: string;
  merchantId?: string;
  iat: number;  // Issued at
  exp: number;  // Expiration
}

Usage:

typescript
const { currentUser } = useRequestContext();

console.log(currentUser.userId);       // "user-123"
console.log(currentUser.email);        // "user@example.com"
console.log(currentUser.organizerId);  // "org-456"
console.log(currentUser.roles);
// [{ id: "...", name: "Admin", identifier: "900_admin" }]

userId

Shortcut for currentUser.userId:

typescript
const { userId } = useRequestContext();

await this.repository.updateById({
  id: recordId,
  data: { updatedBy: userId },
});

roles

Array of role identifier strings, extracted via currentUser.roles.map(r => r.identifier):

typescript
const { roles } = useRequestContext();
// ["999_super-admin", "500_organizer-owner"]

if (roles.includes('500_organizer-owner')) {
  // Allow organizer-specific actions
}

isAlwaysAllowed

Checks if the user holds an always-allowed role. Computed by intersecting the application's AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES binding (configured per app - typically SUPER_ADMIN / ADMIN) with the user's role identifiers:

typescript
// Implementation detail:
const allowedRoles = getApplication().get<string[]>({
  key: AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES,
});
const isAlwaysAllowed = intersection(allowedRoles, roles).length > 0;
// e.g. allowedRoles = ['999_super-admin', '900_admin']

Usage:

typescript
const { isAlwaysAllowed, roles } = useRequestContext();

// Skip detailed checks for admins
if (isAlwaysAllowed) {
  return this.performAction();
}

// Otherwise, check specific permissions
if (!roles.includes('editor')) {
  throw new ForbiddenError();
}

useCountData

A boolean derived from the X-Request-Count-Data HTTP header (defaults to true). Controls whether array responses are wrapped in { data, count } or returned as plain arrays:

typescript
const { useCountData } = useRequestContext();
// true  => responses are { data: [...], count: N }
// false => responses are plain [...]

Clients can disable wrapping by sending:

X-Request-Count-Data: false

normalizeCountableData

Normalizes list query results. Sets Content-Range, X-Response-Format, and X-Response-Count-Data response headers, then returns either { data, count } or a plain array depending on useCountData:

typescript
const { normalizeCountableData } = useRequestContext();

const result = await this.repository.find({ where, limit, offset });
const total = await this.repository.count({ where });

return normalizeCountableData({
  data: result,
  range: { start: offset, end: offset + result.length - 1, total },
});
// If useCountData=true:  { data: [...], count: N }
// If useCountData=false: [...]
// Headers set: Content-Range: records 0-9/100

formatResponse and formatArrayResponse

Convenience methods that call context.json() with the appropriate shape based on useCountData:

typescript
const { formatResponse, formatArrayResponse } = useRequestContext();

// Single item
return formatResponse({ data: product, count: 1 });
// If useCountData=true:  json({ data: product, count: 1 })
// If useCountData=false: json(product)

// Array
return formatArrayResponse({ data: products, count: products.length });
// If useCountData=true:  json({ data: products, count: N })
// If useCountData=false: json(products)

// Custom status code
return formatResponse({ data: created, count: 1 }, 201);

Request Context Flow

Authorization Patterns

Role-Based Access Control

typescript
@service()
export class MerchantService {
  async updateMerchant(id: string, data: UpdateMerchantDto): Promise<TMerchant> {
    const { isAlwaysAllowed, roles, currentUser } = useRequestContext();

    // Super admins can update any merchant
    if (isAlwaysAllowed) {
      return this.merchantRepository.updateById({ id, data });
    }

    // Organizer owners can update their own merchants
    if (roles.includes('500_organizer-owner')) {
      const merchant = await this.merchantRepository.findById({ id });

      if (merchant.organizerId !== currentUser.organizerId) {
        throw new ForbiddenError('Cannot update merchant from another organization');
      }

      return this.merchantRepository.updateById({ id, data });
    }

    throw new ForbiddenError('Insufficient permissions');
  }
}

Resource Ownership Check

typescript
@service()
export class SaleOrderService {
  async getOrderDetails(orderId: string): Promise<TSaleOrder> {
    const { userId, isAlwaysAllowed } = useRequestContext();

    const order = await this.saleOrderRepository.findById({ id: orderId });

    // Admins can see all orders
    if (isAlwaysAllowed) {
      return order;
    }

    // Regular users can only see their own orders
    if (order.customerId !== userId) {
      throw new ForbiddenError('Cannot access order from another user');
    }

    return order;
  }
}

Fixed User Roles Reference

typescript
// Extends the IGNIS AuthorizationRoles (SUPER_ADMIN 999, ADMIN 900, GUEST 1).
// Each role is an AuthorizationRole; identifier = `<priority padded to 3>_<name>`.
export class AppFixedRoles extends AuthorizationRoles {
  static readonly OPERATOR = AuthorizationRole.build({ name: 'operator', priority: 600 }); // 600_operator
  static readonly OWNER = AuthorizationRole.build({ name: 'organizer-owner', priority: 500 }); // 500_organizer-owner
  static readonly CASHIER = AuthorizationRole.build({ name: 'cashier', priority: 110 }); // 110_cashier
  static readonly EMPLOYEE = AuthorizationRole.build({ name: 'employee', priority: 100 }); // 100_employee
  static readonly CUSTOMER = AuthorizationRole.build({ name: 'customer', priority: 10 }); // 010_customer

  static readonly SYSTEM_ROLE_IDENTIFIERS = new Set([
    this.SUPER_ADMIN.identifier, // 999_super-admin
    this.ADMIN.identifier,       // 900_admin
    this.OPERATOR.identifier,    // 600_operator
  ]);

  static isSystemUser(roles?: { identifier: string }[]): boolean { /* ... */ }
  static isOrganizerOwner(roles?: { identifier: string }[]): boolean { /* ... */ }
}

// Note: `isAlwaysAllowed` is NOT derived from this class - it intersects the
// per-app `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` binding with the user's roles.

Audit Trail Patterns

Create with Audit

typescript
async createProduct(data: CreateProductDto): Promise<TProduct> {
  const { userId } = useRequestContext();

  return this.productRepository.create({
    data: {
      id: IdGenerator.getInstance().nextId(),
      ...data,
      createdBy: userId,
      createdAt: new Date(),
    },
  });
}

Update with Audit

typescript
async updateProduct(id: string, data: UpdateProductDto): Promise<TProduct> {
  const { userId } = useRequestContext();

  return this.productRepository.updateById({
    id,
    data: {
      ...data,
      updatedBy: userId,
      updatedAt: new Date(),
    },
  });
}

Soft Delete with Audit

typescript
async deleteProduct(id: string): Promise<void> {
  const { userId } = useRequestContext();

  await this.productRepository.updateById({
    id,
    data: {
      deletedBy: userId,
      deletedAt: new Date(),
    },
  });
}

Error Handling

Missing Context

If useRequestContext() is called outside of an HTTP request lifecycle (e.g., in a background job or during application boot), it throws:

typescript
try {
  const { userId } = useRequestContext();
} catch (error) {
  // [useRequestContext] Request context is undefined.
}

Graceful Fallback for Background Jobs

typescript
function safeGetContext() {
  try {
    return useRequestContext();
  } catch {
    // In non-request context (e.g., background job, event handler)
    return {
      userId: 'system',
      roles: ['system'],
      isAlwaysAllowed: true,
      currentUser: null,
      context: null,
    };
  }
}

Controller Integration

The useRequestContext() function relies on the authentication middleware having populated the CURRENT_USER in the Hono context. Always use @authenticate on controller routes:

typescript
@controller({ basePath: '/products' })
export class ProductController {
  constructor(private productService: ProductService) {}

  @post('/')
  @authenticate(['jwt'])
  async create(@body() data: CreateProductDto): Promise<TProduct> {
    // useRequestContext() is safe to call in the service layer
    return this.productService.createProduct(data);
  }

  @get('/:id')
  @authenticate(['jwt', 'basic'])
  async getById(@param('id') id: string): Promise<TProduct> {
    return this.productService.getProduct(id);
  }
}

Testing

Mock Request Context

typescript
import { useRequestContext } from '@nx/core/utilities';

jest.mock('@nx/core/utilities', () => ({
  useRequestContext: jest.fn(),
}));

describe('ProductService', () => {
  beforeEach(() => {
    (useRequestContext as jest.Mock).mockReturnValue({
      userId: 'test-user-123',
      roles: ['500_organizer-owner'],
      isAlwaysAllowed: false,
      useCountData: true,
      currentUser: {
        userId: 'test-user-123',
        organizerId: 'test-org-456',
        roles: [{ identifier: '500_organizer-owner' }],
      },
    });
  });

  it('creates product with audit trail', async () => {
    const result = await productService.createProduct({ name: 'Test' });
    expect(result.createdBy).toBe('test-user-123');
  });
});

Best Practices

1. Check Authorization Early

typescript
// Correct -- check at start of method
async updateProduct(id: string, data: UpdateProductDto) {
  const { isAlwaysAllowed, roles } = useRequestContext();

  if (!isAlwaysAllowed && !roles.includes('editor')) {
    throw new ForbiddenError();
  }

  // Then proceed with business logic
}

2. Use isAlwaysAllowed for Admin Bypass

typescript
// Correct -- clean admin bypass
if (isAlwaysAllowed) {
  return this.performAction();
}

// Avoid -- checking individual admin roles
if (roles.includes('999_super-admin') || roles.includes('900_admin')) {
  // ...
}

3. Include Context in Logs

typescript
async processOrder(orderId: string) {
  const { userId } = useRequestContext();
  this.logger.info('Processing order | orderId: %s | userId: %s', orderId, userId);
}

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.