Modular RESTlets

I leverage NetSuite RESTlets pretty extensively for several of my projects. When I got tired of keeping track of multiple RESTlet endpoints in various config files for numerous apps, I decided to change my approach.

Instead of creating a new RESTlet for each action or even domain/problem area, I decided to create a single RESTlet that would expose multiple “operations”. Those operations then are just plain old functions that accept a “payload” from the POST request and return any old object, which is JSONified before being sent back to the client. These functions can even live in a totally separate library script, making the solution entirely modular and allowing us to change a single operation while touching as little code as possible.

To do this, I needed to first come up with a “framework” of sorts that allowed me to define these operations. I created a simple request handler that took the request, determined the operation function it needed to call, then wrapped the call to ensure errors were always logged to our external logging server.

This is the interface all POST requests to the RESTlet conform to.

export interface ApiRequest<T> {
    operation: string;
    payload: T;
}

Next, these are the interfaces implemented by each “operation handler”.

interface OperationHandlerCallback<T> {
    (req: T): any;
}

interface OperationHandler<T> {
    name: string;
    callback: OperationHandlerCallback<T>;
}

Finally, this is the “API request handler” that takes in the request and calls the appropriate operation handler.

export class ApiRequestHandler {

    private _opHandlers: OperationHandler<any>[] = [];

    constructor(private _log: Logger) { }

    addOperation<T>(name: string, callback: OperationHandlerCallback<T>): void {
        const existing = this._getOpHandler(name);

        if (existing) {
            this._opHandlers.splice(this._opHandlers.indexOf(existing), 1);
        }

        this._opHandlers.push({ name, callback });
    }

    handleRequest<T>(req?: ApiRequest<T>): NSErrorResponse | any {
        if (!req) {
            return new NSErrorResponse(NSErrorCodes.BAD_REQUEST, "Request body cannot be empty!");
        }

        const opHandler = this._getOpHandler(req.operation);

        if (!opHandler) {
            return new NSErrorResponse(NSErrorCodes.SCRIPT_ERROR, `Failed to find a suitable request handler for "${req.operation}".`);
        }

        let result = null;

        try {
            result = opHandler.callback(req.payload);
        }

        catch (err) {
            result = new NSErrorResponse(NSErrorCodes.SCRIPT_ERROR, err.message);

            this._log.error(`Failed to handle API request.`, {
                err,
                req
            });
        }

        finally {
            this._log.flushEvents();
        }

        return result;
    }

    private _getOpHandler(name: string): OperationHandler<any> | undefined {
        const matches = this._opHandlers.filter(x => x.name === name);

        if (matches.length > 0) {
            return matches[0];
        }

        return undefined;
    }

}

To use the request handler, we simply add one or more operations, then ask it to handle a request!

/**
 * Called by NetSuite when the RESTlet receives a POST request.
 * 
 * @param requestBody The body of the POST request.
 */
export function post(requestBody?: ApiRequest<any>): any | NSErrorResponse {
    const log = LoggerFactory.getLogger();
    const api = new ApiRequestHandler(log);

    api.addOperation<any>("helloWorld", x => {
        return "Hello World!";
    });

    return api.handleRequest(requestBody);
}
Josh Johnson

Josh Johnson

Solutions Architect at TrueCommerce
Josh has been writing software for 7 years. He currently works on the .NET stack using .NET Core and Angular to build highly-customized solutions for the Professional Services team at TrueCommerce.
Josh Johnson

Latest posts by Josh Johnson (see all)