My TypeScript Software Design & Architecture book just prelaunched! Check out solidbook.io.
Close

Clean & Consistent Express.js Controllers | Enterprise Node.js + TypeScript

May 11th, 2019 / 9 min read / Share / Edit on GitHub
In this article, we explore how to structure a clean and consistent Express.js controller by using abstraction and encapsulation with TypeScript.

This article is part of the Enterprise Node.js + TypeScript series.

Express.js is my favourite web framework to use with Node.js for building out the RESTful APIs on my backend applications.

One of the reasons why Express.js is so popular is because it's minimal and allows you to get up and running quickly with pretty much 0 boilerplate code.

Because there's a lack of opinion around how Express.js controllers and routes should be setup, it's easy to end up writing code that's not very DRY and not very consistent.

Not only is it important to keep our code DRY to prevent introducing bugs, but we should aim to keep our API responses consistent. The best developer experiences working with RESTful APIs (think Stripe's API) original from clear and consistent RESTful responses.

Right off the bat, I can think of two ways that we can increase the stability/structure of our responses:

  1. Use Data Transmission Objects (DTOs) to compose a contract for the structure of the response objects (consistent data contracts).
  2. Use a BaseController to encapsulate all responses for all concrete controllers.

We'll cover #2 in this article and revisit #1 in a separate one.

Creating a BaseController

We want to create some sort of BaseController that represents all of the functionality that a controller can accomplish, from one place.

For example, every controller should be able to:

  • take in a request and a response
  • return a 200 with a response payload / dto
  • return a 200/201 without a response payload / dto
  • return a 400 error
  • return a 500 error

To encapsulate all of this functionality, we can use an abstract class.

shared/infra/http/models/BaseController.ts
import * as express from 'express'

export abstract class BaseController {

  /**
   * This is the implementation that we will leave to the
   * subclasses to figure out. 
   */

  protected abstract executeImpl (
    req: express.Request, res: express.Response
  ): Promise<void | any>;

  /**
   * This is what we will call on the route handler.
   * We also make sure to catch any uncaught errors in the
   * implementation.
   */

  public async execute (
    req: express.Request, res: express.Response
  ): Promise<void> {

    try {
      await this.executeImpl(req, res);
    } catch (err) {
      console.log(`[BaseController]: Uncaught controller error`);
      console.log(err);
      this.fail(res, 'An unexpected error occurred')
    }
  }

  public static jsonResponse (
    res: express.Response, code: number, message: string
  ) {
    return res.status(code).json({ message })
  }

  public ok<T> (res: express.Response, dto?: T) {
    if (!!dto) {
      res.type('application/json');
      return res.status(200).json(dto);
    } else {
      return res.sendStatus(200);
    }
  }

  public created (res: express.Response) {
    return res.sendStatus(201);
  }

  public clientError (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 400, message ? message : 'Unauthorized');
  }

  public unauthorized (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 401, message ? message : 'Unauthorized');
  }

  public paymentRequired (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 402, message ? message : 'Payment required');
  }

  public forbidden (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 403, message ? message : 'Forbidden');
  }

  public notFound (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 404, message ? message : 'Not found');
  }

  public conflict (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 409, message ? message : 'Conflict');
  }

  public tooMany (res: express.Response, message?: string) {
    return BaseController.jsonResponse(res, 429, message ? message : 'Too many requests');
  }

  public todo (res: express.Response) {
    return BaseController.jsonResponse(res, 400, 'TODO');
  }

  public fail (res: express.Response, error: Error | string) {
    console.log(error);
    return res.status(500).json({
      message: error.toString()
    })
  }
}

At this point, you may be asking why we have an executeImpl(req, res) in addition to an execute(req, res) method.

The idea is that the execute(req, res) public method exists in order to actually hook up the Express handler to a Router, while the executeImpl(res, res) method is responsible for running the controller logic.

This is done in order to encapsulate the request and response objects to the controller's state and remove the need for us to pass 'em around manually.

If you're still confused, keep following along. It should become clear as we continue!

Implementing a controller

Let's take the basic example of creating a User that requires a valid password, username and email.

Starting a simple controller to create a user might begin looking like this.

users/useCases/createUser/CreateUserController.ts
import * as express from 'express'

export class CreateUserController extends BaseController {
  protected async executeImpl (req: express.Request, res: express.Response): Promise<void | any> {
    try {
      // ... Handle request by creating objects
 
    } catch (err) {
      return this.fail(err.toString())
    }
  }
}

Also, check out that nice auto-completion and all the methods we now have available that we no longer have to manually implement!

Implementing the abstract method

When we extend an abstract class, if the abstract class had any abstract methods on it, we would need to implement those in the subclass.

Observe that we've implemented it in this CreateUserController. If you'll recall in the BaseController, it had a single abstract method:

// Abstract method from the CreateUserController 
protected abstract executeImpl (req: express.Request, res: express.Response): Promise<void | any>;

This is where we will define the controller logic. We'll start by validating the request payload.

Validating the request payload

Let's continue by utilizing the Value Objects to validate that the username, password and email coming in hot off the wire from the internet are valid.

users/useCases/createUser/CreateUserController.ts
import * as express from 'express'

export class CreateUserController extends BaseController {
  protected executeImpl (req: express.Request, res: express.Response): void {
    try {
      const { username, password, email } = req.body;
      const usernameOrError: Result<Username> = Username.create(username);
      const passwordOrError: Result<Password> = Password.create(password);
      const emailOrError: Result<Email> = Email.create(email);

      const result = Result.combine([ 
        usernameOrError, passwordOrError, emailOrError 
      ]);

      if (result.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      // ... continue

    } catch (err) {
      return this.fail(res, err.toString())
    }
  }
}

If you've been following with the Domain Driven-Design articles I've been writing, you'll recall that this is my preferred approach of delegating the responsibility of validating objects.

From within any of these Value Object classes, you can use a combination of Joi validators, custom validation methods and anything else you like in order to ensure that the value object invariants are satisfied.

However you might be seeing something new here.

Result class

And that something is the usage of a Result class. Here's a detailed explanation of it.

For now, it's sufficient to understand that the Result class is a cleaner way to handle errors rather than throwing errors explicitly. It also allows us to combine(Result[]?)s together in order and will return the first of the provided array of Results that is invalid.

This in turn helps to pass back really helpful contextual error messages to the client consuming this API.


Finishing it up

In order to finish off this API request, we'll want to persist the User to persistence.

To do that, we'll ensure that we utilize Depdendency Inversion to specify that this class depends on an IUserRepo.

users/useCases/createUser/CreateUserController.ts
import * as express from 'express'

export class CreateUserController extends BaseController {
  private userRepo: IUserRepo;

  constructor (userRepo: IUserRepo) {
    super();
    this.userRepo = userRepo;
  }

  protected async executeImpl (req: express.Request, res: express.Response): Promise<void | any> {
    try {
      const { username, password, email } = this.req.body;
      const usernameOrError: Result<Username> = Username.create(username);
      const passwordOrError: Result<Password> = Password.create(password);
      const emailOrError: Result<Email> = Email.create(email);

      const result = Result.combine([ 
        usernameOrError, passwordOrError, emailOrError 
      ]);

      if (result.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      // ... continue
      const userOrError: Result<User> = User.create({
        username: usernameOrError.getValue(),
        password: passwordOrError.getValue(),
        email: emailOrError.getValue()
      });

      if (userOrError.isFailure) {
        // Send back a 400 client error
        return this.clientError(res, result.error);
      }

      const user: User = userOrError.getValue();

      // Create the user
      await this.userRepo.createUser(user);

      // Return a 200
      return this.ok<any>(res);

    } catch (err) {
      return this.fail(res, err.toString())
    }
  }
}

That's it for the controller!

Hooking it up to an Express.js route

To export features from a module without needing a DI container, I like to do something like this:

users/useCases/createUser/index.ts
import { CreateUserController } from './CreateUserController'
import { UserRepo } from '../repos/UserRepo';
import { models } from '../infra/sequelize';

const userRepo = new UserRepo(models);
const createUserController = new CreateUserController(userRepo);

// Export the feature 
export { createUserController };

And to hook it up, we could create a separate router, add any middleware we need to (two are shown here for example) and then execute the controller like so:

import { createUserController } from '../useCases/createUser'

import * as express from 'express'
import { Router } from 'express'
const userRouter: Router = Router();

userRouter.post('/new', 
  middleware.useCORS,
  middleware.rateLimit,
  // + any other middleware 
  ...
  (req, res) => createUserController.execute(req, res)
);

export { userRouter }

Finally, we can import it from our main Express.js app instance.

// app.js

import { userRouter } from '../users/http/routers'

const app = express();
app.use('/user', userRouter)

And voila! A POST to /user/new should do the trick.


Now you know my preferred way to create Express.js controllers by encapsulating all of the common functionality in an abstract class.

This type of thing is also possible to do without TypeScript but it's requires a little bit more trickery in order to implement the design patterns and principles we've mentioned in this article.

If you're still considering whether you want to use TypeScript, check out my definitive guide on whether it makes sense for your next project.

Update: May 14th, 2019

Thanks to @patroza for suggesting that we maintain the request and response objects from within the base class in a stateful/truly object-oriented manner, rather than passing them around functionally. This approach encapsulates responsibility much better.

Update December 2nd, 2019

After some battle-testing on this approach, the community and I discovered that making the response on the base class stateful isn't a good idea. If several requests are coming enter the same controller, we'd overwrite responses. The functional approach of passing req and response through has 0 side effects and seems to eliminate this issue. Thanks Ryan, Sam, and Matt!



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


3 Comments

Submit
Daniel
4 months ago

Your abstract execute function should be async

Marth
2 months ago

Hey, Are you creating a Controller for each Functionality ? (Because I cannot see how you can know in advanced all of the possible controller functions , and if so the base controller will be way to heavy in functions from all of the program) , So I am trying to understand how the base controller helping you , and isn't it anti pattern to forward the req , res to the controller ? (shouldn't you create a object that the controller suppose to get a built object that he knows how to work with ? ) Thanks in advanced.

Khalil Stemmler
2 months ago

Hey Marth.


The base controller has been helping me keep Use Case controllers DRY. I don't have to remember all of the error codes and what they mean anymore. Using functional error handling, I can map errors to the correct type of response.


Yes, every Use Case has its own controller. Check out a couple use case controllers from DDDForum. Here's a good one.


The way that this works is the `abstract` `executeImpl` method.


Its in that method that I put my use case-specific controller code in.


That phenomenon is known as Inversion of Control or the "Hollywood Principle (Don't call us, we'll call you)". Not to be confused with Dependency Inversion or Dependency Injection.

ryan
a month ago

Hi, first of all, great job! But the 'this.res = res;' statement seems have concurrent conflict issue, because the BaseController instance would be singleton.

Khalil Stemmler
a month ago

Good call, Ryan. Let's roll this back to the functional approach I used earlier. I'll update the article.


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Enterprise Node + TypeScript



You may also enjoy...

A few more related articles

Functional Error Handling with Express.js and DDD | Enterprise Node.js + TypeScript
Aug 14th, 2019 / 16 min read
How to expressively represent (database, validation and unexpected) errors as domain concepts using functional programming concept...
Flexible Error Handling w/ the Result Class | Enterprise Node.js + TypeScript
May 15th, 2019 / 9 min read
Purposefully throwing errors can have several negative side effects to the readability and traceability of your code. In this arti...
Better Software Design with Application Layer Use Cases | Enterprise Node.js + TypeScript
Jun 25th, 2019 / 16 min read
In this article, we explore how organizing application logic as Use Cases in the application layer helps to make large typescript ...
Knowing When CRUD & MVC Isn't Enough | Enterprise Node.js + TypeScript
May 24th, 2019 / 13 min read
MVC the granddaddy of application architectures. In this article, we explore common MVC patterns, the responsibilities of the "M"-...

Want to be notified when new content comes out?

Join 4000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates