Reference

Custom Actions

Creating custom API actions in dnax Framework.

Custom actions let you define business logic beyond standard CRUD operations.

Define Custom Actions

import { define } from '@dnax/core';

export default define.Collection({
  slug: 'orders',
  fields: [
    { name: 'status', type: 'enum', enumOptions: { items: ['pending', 'processing', 'completed'] } },
    { name: 'total', type: 'number' },
  ],
  actions: {
    // Custom action
    processOrder: async ({ rest, data }) => {
      const order = await rest.findOne('orders', data.orderId);
      
      await rest.updateOne('orders', data.orderId, {
        $set: { status: 'processing' }
      });
      
      return { success: true, order };
    },
    
    // Another custom action
    cancelOrder: async ({ rest, data }) => {
      await rest.updateOne('orders', data.orderId, {
        $set: { status: 'cancelled' }
      });
      
      return { success: true };
    },
  },
});

Call Custom Actions

POST /api/:tenant_id/orders/processOrder
Content-Type: application/json

{
  "data": {
    "orderId": "64f1a2b3c4d5e6f7a8b9c0d1"
  }
}

Response:

{
  "success": true,
  "order": { ... }
}

Action Context

The action receives this context:

{
  rest: useRest;    // Rest instance for database operations
  data: any;      // Request body data
  error: function; // Error helper
}

Using Rest in Actions

Access the full REST API within custom actions:

actions: {
  // Get user orders
  getUserOrders: async ({ rest, data }) => {
    const orders = await rest.find('orders', {
      $match: { userId: data.userId },
      $sort: { createdAt: -1 }
    });
    
    return orders;
  },
  
  // Bulk update
  archiveOldOrders: async ({ rest, data }) => {
    const oldOrders = await rest.find('orders', {
      $match: { 
        createdAt: { $lt: new Date(data.cutoffDate) },
        status: 'completed'
      }
    });
    
    const ids = oldOrders.map(o => o._id);
    const result = await rest.updateMany('orders', ids, {
      $set: { archived: true }
    });
    
    return { updated: result.modifiedCount };
  },
  
  // Call another collection
  duplicateToArchive: async ({ rest, data }) => {
    const order = await rest.findOne('orders', data.orderId);
    
    await rest.insertOne('orders_archive', {
      ...order,
      originalId: order._id,
      archivedAt: new Date(),
    });
    
    return { success: true };
  },
}

Error Handling

Throw custom errors:

actions: {
  processOrder: async ({ rest, data, error }) => {
    const order = await rest.findOne('orders', data.orderId);
    
    if (!order) {
      throw error('Order not found', {
        code: 'ORDER_NOT_FOUND',
        status: 404,
      });
    }
    
    if (order.status === 'cancelled') {
      throw error('Cannot process cancelled order', {
        code: 'ORDER_CANCELLED',
        status: 400,
      });
    }
    
    // Process...
  },
}

Validation

Combine with field validation:

actions: {
  // Validation is handled by the field schema
  createWithPayment: async ({ rest, data }) => {
    // data has been validated against field schema
    const order = await rest.insertOne('orders', data);
    
    // Process payment...
    return { success: true, order };
  },
}
Copyright © 2026