Skip to main content
  1. Posts/

Refactoring with routing-controllers: 1. Middleware

·695 words·4 mins
Table of Contents

Why I changed it #

Authentication in express-sequelize-ts uses the Passport local, jwt, and jwt-refresh strategies. The first version kept a callback for each strategy in one object and selected the callback from a shared function.

It worked, but the rest of the controllers had already moved to routing-controllers while authentication remained a group of functions and callbacks. This post only covers that middleware refactor, not the entire repository.

The previous structure #

The old auth.middleware.ts selected a strategy, ran Passport, assigned the user, and wrote authentication errors in the same flow.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type TAuthType = 'local' | 'jwt' | 'jwt-refresh';

const verifyCallback = {
  local:
    (req: Request, resolve: any, reject: any) =>
    (err: any, user: Model<User>, info: any): void => {
      if (err || !user) {
        return reject(new ApiError(httpStatus.UNAUTHORIZED, info?.message || err.message));
      }

      req.user = user;
      resolve();
    },
  jwt: (req: Request, resolve: any, reject: any) => (err: any, user: Model<User>, info: any) => {
    // ...
  }
};

const authMiddlewareFn = (req: Request, res: Response, next: NextFunction, authType: TAuthType) =>
  new Promise((resolve, reject) => {
    passport.authenticate(authType, { session: false }, verifyCallback[authType](req, resolve, reject))(req, res, next);
  });

Adding another authentication type also made the callback object and its branching logic larger. The same function handled both the authentication flow and the HTTP error response, which made the boundary less clear.

Splitting it into middleware classes #

routing-controllers accepts middleware classes that implement ExpressMiddlewareInterface. The use() method is the Express middleware entry point.

The project extends that contract with the authentication fields it needs. The example below omits the detailed callback types and keeps only the shared shape.

1
2
3
4
5
6
7
8
export type TAuthType = 'local' | 'jwt' | 'jwt-refresh';

export interface IAuthMiddleware<TUser> extends ExpressMiddlewareInterface {
  authType: TAuthType;
  verifyCallback: (...args: any[]) => (...args: any[]) => void;
  authenticate: (...args: any[]) => any;
  use(req: Request, res: Response, next: NextFunction): void;
}

IAuthMiddleware gives every authentication class the same basic shape. The repository currently uses this structure for LocalAuthMiddleware and JWTRefreshAuthMiddleware.

This is the core flow of JWTRefreshAuthMiddleware with unrelated details removed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service()
export class JWTRefreshAuthMiddleware implements IAuthMiddleware<Model<User>> {
  public readonly authType: TAuthType = 'jwt-refresh';

  public readonly verifyCallback =
    (req: Request, res: Response, next: NextFunction) =>
    (err: any, user: Model<User>, info: any): void => {
      if (err || !user) {
        return next(new HttpError(httpStatus.UNAUTHORIZED, info?.message || err.message));
      }

      req.user = user;
      next();
    };

  public readonly authenticate = (callback: any) =>
    passport.authenticate(this.authType, { session: false }, callback);

  public use(req: Request, res: Response, next: NextFunction): void {
    this.authenticate(this.verifyCallback(req, res, next))(req, res, next);
  }
}

use() now runs Passport with the strategy owned by the class. On failure, it passes a HttpError to next() instead of creating a response inside the authentication middleware. The project’s HttpErrorHandler turns that error into the final HTTP response.

The failure branch must stop with return next(error). Otherwise, the same request can call next() again after an authentication failure.

Applying it to controller actions #

The middleware classes are attached to the actions that need them with @UseBefore.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Post('/sign-in')
@UseBefore(LocalAuthMiddleware)
public async signIn(@Body() userBody: SignInUserDto, @Res() res: Response) {
  // ...
}

@Get('/refresh-token')
@UseBefore(JWTRefreshAuthMiddleware)
public async refreshToken(@Req() req: Request, @Res() res: Response) {
  // ...
}

The controller now shows which authentication flow each action uses. Protected JWT routes follow a separate path through the project’s authorizationChecker and @Authorized() instead of forcing every authentication case into the same middleware class.

Conclusion #

The useful change was the boundary, not the number of lines removed. Each middleware class owns one Passport flow, the controller declares where it runs, and the error middleware owns the response.

This creates one class per authentication type. I prefer that small cost to growing one callback object because it makes the next change easier to locate.

Next in the series: Refactoring with routing-controllers: 2. Interceptor

References #