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: "998-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", "899-organizer-owner"]

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

isAlwaysAllowed

Checks if the user holds a SUPER_ADMIN or ADMIN role. Computed by intersecting FixedUserRoles.ALWAYS_ALLOW_ROLES with the user's role identifiers:

typescript
// Implementation detail:
const isAlwaysAllowed =
  intersection(Array.from(FixedUserRoles.ALWAYS_ALLOW_ROLES), roles).length > 0;

// FixedUserRoles.ALWAYS_ALLOW_ROLES = Set(['999-super-admin', '998-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('899-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
export class FixedUserRoles {
  // System-level roles
  static readonly SUPER_ADMIN = '999-super-admin';
  static readonly ADMIN = '998-admin';
  static readonly OPERATOR = '997-operator';

  // Organization-level roles
  static readonly ORGANIZER_OWNER = '899-organizer-owner';
  static readonly EMPLOYEE = '898-employee';

  // Auto-allow set (used by isAlwaysAllowed)
  static readonly ALWAYS_ALLOW_ROLES = new Set([
    FixedUserRoles.SUPER_ADMIN,
    FixedUserRoles.ADMIN,
  ]);

  // Priority codes (higher = more privilege)
  static readonly PRIORITY_CODE = {
    SUPER_ADMIN: 999,
    ADMIN: 998,
    OPERATOR: 997,
    ORGANIZER_OWNER: 899,
    EMPLOYEE: 898,
  };
}

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: ['899-organizer-owner'],
      isAlwaysAllowed: false,
      useCountData: true,
      currentUser: {
        userId: 'test-user-123',
        organizerId: 'test-org-456',
        roles: [{ identifier: '899-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('998-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.