@nicholasdigital/api-controller
v0.0.12
Published
A decorator-based API controller framework for Express.js applications with Socket.IO support, built-in error handling, flow metrics, and transaction management.
Downloads
36
Readme
@nicholasdigital/api-controller
A decorator-based API controller framework for Express.js applications with Socket.IO support, built-in error handling, flow metrics, and transaction management.
Installation
npm install @nicholasdigital/api-controllerQuick Start
import express from 'express';
import Controller from '@nicholasdigital/api-controller';
const { Method, Socket, Exception, Response } = Controller;
// Define a controller class
@Controller('/api/users')
class UserController {
@Method('get', '/')
async getUsers(req, res) {
const users = await getUsersFromDatabase();
res.json(Response.success(users));
}
@Method('post', '/', { useTransaction: true })
async createUser(req, res) {
const user = await createUserInDatabase(req.body);
res.json(Response.success(user));
}
@Socket('/notifications')
handleNotifications(socket) {
socket.on('connection', (client) => {
client.emit('welcome', 'Connected to notifications!');
});
}
}
// Setup Express app
const app = express();
const server = require('http').createServer(app);
// Link controller methods to Express router
UserController.linkMethods(app);
// Link socket handlers to Socket.IO server
UserController.linkSockets(server);
server.listen(3000);Error Handling System
The framework provides a comprehensive error handling system that maps thrown exceptions to HTTP responses. Here's how it works:
1. Exception Throwing
Use the Exception class to throw structured errors with a code and optional arguments:
import { Exception } from '@nicholasdigital/api-controller';
// Throw an exception with just a code
throw new Exception('USER_NOT_FOUND');
// Throw an exception with code and arguments
throw new Exception('VALIDATION_FAILED', 'email', 'username');
// Throw an exception with multiple arguments
throw new Exception('INSUFFICIENT_FUNDS', accountId, requestedAmount, availableBalance);2. Error Handler Definition
Define error handlers in your controller decorator that map exception codes to user-friendly messages:
@Controller('/api/users', {
errors: {
// Simple error handler - no arguments
USER_NOT_FOUND: () => 'The requested user could not be found',
// Error handler with arguments from the exception
VALIDATION_FAILED: (field1, field2) => `Validation failed for fields: ${field1}, ${field2}`,
// Error handler with complex logic
INSUFFICIENT_FUNDS: (accountId, requested, available) =>
`Account ${accountId} has insufficient funds. Requested: $${requested}, Available: $${available}`
},
errorCodes: {
USER_NOT_FOUND: 404,
VALIDATION_FAILED: 400,
INSUFFICIENT_FUNDS: 402
}
})
class UserController {
@Method('get', '/:id')
async getUser(req, res) {
const user = await findUser(req.params.id);
if (!user) {
// This will trigger the USER_NOT_FOUND error handler
throw new Exception('USER_NOT_FOUND');
}
res.json(Response.success(user));
}
@Method('post', '/transfer')
async transferFunds(req, res) {
const { fromAccountId, amount } = req.body;
const account = await getAccount(fromAccountId);
if (account.balance < amount) {
// Arguments passed to exception are forwarded to error handler
throw new Exception('INSUFFICIENT_FUNDS', fromAccountId, amount, account.balance);
}
// Process transfer...
res.json(Response.success({ transferId: req.id }));
}
}3. How Exception-to-Response Mapping Works
When an exception is thrown, the framework:
- Catches the exception in the
apiExceptionCatchermiddleware - Extracts the error code from
exception.code - Looks up the error handler in the merged error handlers (controller + middleware errors)
- Calls the handler function with the arguments from
exception.args - Gets the HTTP status code from the error codes mapping
- Sends a structured JSON response:
// Response format for errors
{
"success": false,
"requestId": "uuid-generated-request-id",
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "Account ACC123 has insufficient funds. Requested: $500, Available: $200"
}
}4. Built-in Error Handling
The framework includes some built-in error types:
// From errors.js
throw new Exception('MISSING_REQUIRED_ARGS', ['email', 'password']);
// Results in: "The request was missing the required arguments: email, password"5. Fallback for Unknown Errors
If an exception is thrown with a code that has no handler:
throw new Exception('UNKNOWN_ERROR_CODE');The framework will:
- Use a default message: "An unknown error occurred"
- Use HTTP status code 200 (default)
- Still log the original exception for debugging
6. Middleware Error Handlers
Middleware can also define error handlers that get merged with controller handlers:
const authMiddleware = {
fn: (requiredRole) => (fn) => async (req, res, next) => {
if (!req.user) {
throw new Exception('NOT_AUTHENTICATED');
}
if (req.user.role !== requiredRole) {
throw new Exception('INSUFFICIENT_PERMISSIONS', requiredRole, req.user.role);
}
return fn(req, res, next);
},
options: ['requiredRole'],
errors: {
NOT_AUTHENTICATED: () => 'Authentication required',
INSUFFICIENT_PERMISSIONS: (required, actual) =>
`Access denied. Required role: ${required}, your role: ${actual}`
},
errorCodes: {
NOT_AUTHENTICATED: 401,
INSUFFICIENT_PERMISSIONS: 403
}
};
Controller.middleware.push(authMiddleware);7. Error Handler Priority
Error handlers are merged in this order (later ones override earlier ones):
- Built-in framework errors
- Middleware error handlers (in registration order)
- Controller error handlers
Complete Example
@Controller('/api/banking', {
errors: {
ACCOUNT_NOT_FOUND: (accountId) => `Account ${accountId} does not exist`,
INSUFFICIENT_FUNDS: (accountId, requested, available) =>
`Account ${accountId} has insufficient funds. Requested: $${requested}, Available: $${available}`,
INVALID_AMOUNT: (amount) => `Invalid transfer amount: $${amount}`,
TRANSFER_FAILED: (reason) => `Transfer failed: ${reason}`
},
errorCodes: {
ACCOUNT_NOT_FOUND: 404,
INSUFFICIENT_FUNDS: 402,
INVALID_AMOUNT: 400,
TRANSFER_FAILED: 500
}
})
class BankingController {
@Method('post', '/transfer')
async transfer(req, res) {
const { fromAccountId, toAccountId, amount } = req.body;
// Validation
if (amount <= 0) {
throw new Exception('INVALID_AMOUNT', amount);
}
// Get accounts
const fromAccount = await getAccount(fromAccountId);
if (!fromAccount) {
throw new Exception('ACCOUNT_NOT_FOUND', fromAccountId);
}
const toAccount = await getAccount(toAccountId);
if (!toAccount) {
throw new Exception('ACCOUNT_NOT_FOUND', toAccountId);
}
// Check balance
if (fromAccount.balance < amount) {
throw new Exception('INSUFFICIENT_FUNDS', fromAccountId, amount, fromAccount.balance);
}
// Attempt transfer
try {
const result = await processTransfer(fromAccountId, toAccountId, amount);
res.json(Response.success(result));
} catch (error) {
throw new Exception('TRANSFER_FAILED', error.message);
}
}
}When errors occur, clients receive consistent, user-friendly error responses while the server logs detailed error information for debugging.
Controllers
Controllers are classes decorated with the @Controller decorator that define a base path and configuration:
@Controller('/api/v1', {
errors: {
USER_NOT_FOUND: () => 'User not found'
},
errorCodes: {
USER_NOT_FOUND: 404
}
})
class ApiController {
// methods and sockets defined here
}HTTP Methods
Use the @Method decorator to define HTTP endpoints:
@Method('get', '/users/:id', {
parser: bodyParser.json(),
useTransaction: false,
flow: { hash: 'get-user' }
})
async getUser(req, res) {
const user = await findUser(req.params.id);
if (!user) {
throw new Exception('USER_NOT_FOUND');
}
res.json(Response.success(user));
}Method Options:
parser: Body parser middleware (default:bodyParser.json())useTransaction: Enable transaction support with rollback capabilitiesflow: Custom flow configuration for metrics tracking
Socket.IO Integration
Define WebSocket handlers with the @Socket decorator:
@Socket('/chat', {
socketConfig: {
cors: { origin: "*" }
}
})
handleChat(socket) {
socket.on('connection', (client) => {
client.on('message', (data) => {
socket.emit('broadcast', data);
});
});
}Transactions
Enable automatic transaction management with rollback support:
@Method('post', '/transfer', { useTransaction: true })
async transferFunds(req, res) {
const { fromAccount, toAccount, amount } = req.body;
// Register rollback functions
req.transaction.pushRollback('debit', async (err) => {
await refundAccount(fromAccount, amount);
});
await debitAccount(fromAccount, amount);
await creditAccount(toAccount, amount);
res.json(Response.success({ transferId: req.id }));
}Response Utilities
Use the built-in Response utility for consistent API responses:
// Success response
res.json(Response.success({ user: userData }));
// Error response
res.json(Response.fail({ code: 'ERROR_CODE', message: 'Error message' }));
// Manual response
res.json(Response(true, userData, null));Middleware
Register global middleware that applies to all controller methods:
// Define middleware
const authMiddleware = (requiredRole) => (fn) => async (req, res, next) => {
if (!req.user || req.user.role !== requiredRole) {
throw new Exception('UNAUTHORIZED');
}
return fn(req, res, next);
};
// Register middleware
Controller.middleware.push({
fn: authMiddleware,
options: ['requiredRole'],
errors: {
UNAUTHORIZED: () => 'Access denied'
},
errorCodes: {
UNAUTHORIZED: 401
}
});
// Use in method options
@Method('delete', '/admin', { requiredRole: 'admin' })
async adminAction(req, res) {
// This method requires admin role
}Flow Metrics
The framework automatically tracks request metrics and performance:
// Custom flow callback for metrics
Controller.flowCallback = (flowData) => {
console.log('Flow metrics:', flowData);
};
// Custom flow configuration
@Method('get', '/heavy-operation', {
flow: { hash: 'heavy-op-v1' }
})
async heavyOperation(req, res) {
// Metrics automatically tracked with custom hash
}License
ISC
