npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-controller

Quick 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:

  1. Catches the exception in the apiExceptionCatcher middleware
  2. Extracts the error code from exception.code
  3. Looks up the error handler in the merged error handlers (controller + middleware errors)
  4. Calls the handler function with the arguments from exception.args
  5. Gets the HTTP status code from the error codes mapping
  6. 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):

  1. Built-in framework errors
  2. Middleware error handlers (in registration order)
  3. 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 capabilities
  • flow: 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