Search code examples
node.jserror-handlingarchitecturedomain-driven-designdesign-by-contract

Node.JS service layer design


I have a very simple express js server that accepts a request from a client, performs some business logic and respond back to the client. The request - response pipeline is handled by a controller and the business logic is performed inside the service layer. The code is functional but I am not sure the way I return errors to the controller from the service layer is correct.

The controller looks something like this:

async createAnOrder(req, res, next) {
    try {

        // Input validations
        if (!req.body.name){
            throw new ClientFacingError(ERROR_CODES.BAD_REQUEST, "Name is missing")
        }

        let newOrder = new Order();
        let validationDictionary = new ValidationDictionary();

        if (!await orderService.createOrder(newOrder, validationDictionary)){
            return next(new ClientFacingError(ERROR_CODES.BAD_REQUEST, validationDictionary.getErrorMessage()));
        }

        res.json({
            orderId: newOrder.id
        })

    } catch (err) {
        if (err instanceof ClientFacingError) {
            res.status(err.code).json({ message: err.message })
        } else {
            res.status(500).json({ message: "Internal Error" })
        }
    }
}

The orderService looks something like this:

async createOrder(newOrder, validationDictionary) {

    // Business logic validation
    if (await orderDb.hasOrder(newOrder)) {
        validationDictionary.addError("Invalid Order", "Order already exists");
        return false;
    }

    await orderDb.createOrder(newOrder);
    return true;
}

I am trying to aim for separation of concern between the business layer to everything else. I also want a smart contract between the service layer methods and the controllers.

I think what I was trying to do was:

  1. Input validation happens in the controller (infrastructure) layer and business related validations only happen in the service layer
  2. Service layer methods return true if operation was successful and false if there are business validation errors.
  3. Any errors thrown by the service layer methods are Runtime errors which is caught by controllers and returned to the client as an Internal Error.

Another way is to adhere to the CQS principle. The service layer method should not return a value but instead throw an exception to indicate failure. The problem with this approach is that the controllers do not have context of whether or not the error message can be returned to the client. I may be able to throw ClientFacingError exception from the service layer which can be used by the controller to decide whether or not the error message can be returned to the client but that feels like I'm coupling the service layer to the infrastructure layer.


Solution

  • You have good concerns and a nice approach, let me just add some thoughts:

    • validation can be part of the service, separated from the main logic. So along createOrder you will also have a validateOrder (in your service that is). From your controller you can use it like this:

    Controller:

    try {
        ...
        orderService.validateOrder(order);
        orderService.createOrder(order);
        ...
    } catch (e) { 
        //handle e
    }
    

    The way to approach this is either throw an exception (like in my example), return a mix of boolean + error code, message..., or simply embed it in createOrder. This depends on your project needs and your taste.

    • I would throw exceptions with concise message and type from the orderService if something went wrong. Code will be more elegant. Also it would be great to return the actual newly created order object if everything went well, but a boolean would suffice at first. (or even void, considering you throw exceptions).

    • Catch the exceptions in your controller, log them, send appropriate response to client.