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
import { useRequestContext } from '@nx/core';
// or
import { useRequestContext } from '@nx/core/utilities';Return Type
The useRequestContext() function returns an object with the following properties:
| Property | Type | Description |
|---|---|---|
context | Context | The raw Hono request context object |
currentUser | IJWTTokenPayload | Full decoded JWT payload |
userId | string | Shortcut for currentUser.userId |
roles | string[] | Array of role identifier strings extracted from currentUser.roles |
isAlwaysAllowed | boolean | true if user has SUPER_ADMIN or ADMIN role |
useCountData | boolean | Whether 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?) => Response | Format a single-item response |
formatArrayResponse | <T>(result, statusCode?) => Response | Format an array response |
Basic Usage
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:
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:
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:
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):
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:
// 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:
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:
const { useCountData } = useRequestContext();
// true => responses are { data: [...], count: N }
// false => responses are plain [...]Clients can disable wrapping by sending:
X-Request-Count-Data: falsenormalizeCountableData
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:
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/100formatResponse and formatArrayResponse
Convenience methods that call context.json() with the appropriate shape based on useCountData:
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
@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
@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
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
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
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
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:
try {
const { userId } = useRequestContext();
} catch (error) {
// [useRequestContext] Request context is undefined.
}Graceful Fallback for Background Jobs
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:
@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
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
// 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
// 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
async processOrder(orderId: string) {
const { userId } = useRequestContext();
this.logger.info('Processing order | orderId: %s | userId: %s', orderId, userId);
}