Nexus NF

Controllers

Learn how to create and configure controllers in NexusNF

Controllers

Controllers are the foundation of NexusNF applications. They group related endpoints under a common namespace and provide configuration options for request handling.

Creating a Controller

Create a controller by extending the ControllerBase class:

import { ControllerBase, Endpoint } from 'nexusnf';

export class UserController extends ControllerBase {
  constructor() {
    super('users');
  }

  @Endpoint('create')
  async createUser(data: any) {
    // Endpoint subject: users.create
    return { id: 123, name: data.name };
  }

  @Endpoint('find')
  async findUser(data: { id: number }) {
    // Endpoint subject: users.find
    return { id: data.id, name: 'John Doe' };
  }
}

// All endpoints will be prefixed with 'users.'
// - users.create
// - users.find

Instantiation

Controllers are instantiated and registered using the NexusApp instance.

const app = new NexusApp(nc, service);

// Register controller
app.registerController(new UserController());

The group name becomes the subject prefix for all endpoints in the controller.

Controller Options

Controllers accept optional configuration to customize behavior:

Queue Groups

Queue groups enable load balancing across multiple service instances. Only one member of a queue group will receive each message:

export class PaymentController extends ControllerBase {
  constructor() {
    super('payment', { queue: 'payment-processors' });
  }

  @Endpoint('process')
  async processPayment(data: any) {
    return { transactionId: 'txn_123' };
  }
}

// Register as usual
app.registerController(new PaymentController());

When you run multiple instances of this service, NATS will distribute requests across all instances in the payment-processors queue group.

Queue groups are essential for horizontal scaling. Without them, all service instances will receive every message.

Endpoint Decorator

The @Endpoint decorator marks methods as service endpoints:

@Endpoint(name: string, options?: EndpointOptions)

Basic Usage

export class MathController extends ControllerBase {
  @Endpoint('add')
  async add(data: { a: number; b: number }) {
    return { result: data.a + data.b };
  }
}

With Validation Schema

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(18)
});

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: userSchema })
  async createUser(data: z.infer<typeof userSchema>) {
    // Data is automatically validated by zod
    return { id: 123, ...data };
  }
}

Binary Data Handling

Process raw binary data instead of JSON. This is useful for e.g., files or non-JSON messages:

export class FileController extends ControllerBase {
  @Endpoint('upload', { asBytes: true })
  async uploadFile(data: Uint8Array) {
    return {
      size: data.length,
      processed: true
    };
  }
}

Endpoint-Specific Queue

Override the controller's queue group for specific endpoints:

export class OrderController extends ControllerBase {
  constructor() {
    super('orders', { queue: 'order-workers' });
  }

  @Endpoint('create')
  async createOrder(data: any) {
    // Uses controller's default queue
    return { orderId: 'ord_123' };
  }

  @Endpoint('process', { queue: 'priority-processors' })
  async processPriorityOrder(data: any) {
    // Uses endpoint-specific queue
    return { orderId: 'ord_456', priority: true };
  }
}

Endpoint Metadata

Add custom metadata to endpoints for documentation or routing:

export class ApiController extends ControllerBase {
  @Endpoint('data', {
    metadata: {
      version: '2.0',
      description: 'Fetches user data'
    }
  })
  async getData(data: any) {
    return { data: 'example' };
  }
}

Metadata is available through the NATS service discovery API and can be used by monitoring tools or API gateways.

Method Signatures

Endpoint handlers receive two parameters:

async handler(data: any, headers?: MsgHdrs): Promise<any>
  • data: The parsed message payload (JSON, string, or Uint8Array)
  • headers: Optional NATS message headers

Using Headers

import { NatsError, type MsgHdrs } from 'nats';

export class AuthController extends ControllerBase {
  @Endpoint('verify')
  async verifyToken(data: any, headers?: MsgHdrs) {
    const token = headers?.get('Authorization');

    if (!token) {
      throw new NatsError('Missing authorization header', '401');
    }

    return { valid: true, userId: 123 };
  }
}

Async vs Sync Handlers

Both synchronous and asynchronous handlers are supported:

export class DataController extends ControllerBase {
  @Endpoint('fetch')
  async fetchData(data: { id: string }) {
    const result = await database.query(data.id);
    return result;
  }
}
export class MathController extends ControllerBase {
  @Endpoint('add')
  add(data: { a: number; b: number }) {
    return { result: data.a + data.b };
  }
}

The framework automatically handles both sync and async handlers. Use async when performing I/O operations.

Error Handling

Throwing errors in handlers automatically generates error responses:

import { NatsError } from 'nats';

export class UserController extends ControllerBase {
  @Endpoint('find')
  async findUser(data: { id: number }) {
    const user = await database.findUser(data.id);

    if (!user) {
      throw new NatsError('User not found', '404');
    }

    return user;
  }
}

Response:

{
  "error": true,
  "message": "User not found",
  "code": "404",
  "details": {
    "name": "Error",
    "message": "User not found"
  }
}

Learn more about error handling in the Error Handling guide.

Best Practices

1. Make use of TypeScript for Type Safety

interface CreateUserInput {
  name: string;
  email: string;
  age: number;
}

interface UserResponse {
  id: number;
  name: string;
  email: string;
}

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: userSchema })
  async createUser(data: CreateUserInput): Promise<UserResponse> {
    return {
      id: 123,
      name: data.name,
      email: data.email
    };
  }
}

Group related functionality in the same controller:

export class UserController extends ControllerBase {
  @Endpoint('create')
  async create(data: any) { /* ... */ }

  @Endpoint('update')
  async update(data: any) { /* ... */ }

  @Endpoint('delete')
  async delete(data: any) { /* ... */ }

  @Endpoint('find')
  async find(data: any) { /* ... */ }
}

3. Use Validation Schemas

Validate input data with Zod schemas:

import { z } from 'zod';

const schemas = {
  create: z.object({
    name: z.string().min(1),
    email: z.string().email()
  }),
  update: z.object({
    id: z.number(),
    name: z.string().min(1).optional(),
    email: z.string().email().optional()
  })
};

export class UserController extends ControllerBase {
  @Endpoint('create', { schema: schemas.create })
  async create(data: z.infer<typeof schemas.create>) { /* ... */ }

  @Endpoint('update', { schema: schemas.update })
  async update(data: z.infer<typeof schemas.update>) { /* ... */ }
}

4. Leverage Queue Groups for Scaling

export class OrderController extends ControllerBase {
  constructor() {
    super('orders', { queue: 'order-processors' });
  }
}

Next Steps