Skip to main content

Interceptor Middlewares

How To Intercept Requests In Arkos

Although Arkos handles the API endpoints generation automatically with basically all you will need for for well structured RESTful API, there are cases where you need to do some specific things related to your own application business logic and if we where working with plain Express we would use the middlewares.

Hence Arkos allows you to even intercept those request that are automatically handled and let you write middlewares to do whatever you want as if you where writing plain express app.

Writing Express Middlewares To Handle Custom Business Logic Before And After Requests

On a conventional Express apps you intercept request with middlewares by adding them directly in the router, for example:

// src/routers/post.router.ts
import { Router } from "express";
import * as postController from "./post.controller.ts";
import {
someProcessingBefore,
someProcessingAfter,
} from "./post.middlewares.ts";

const router = Router();

router.post(
"/api/posts",
someProcessingBefore,
postController.createPost,
someProcessingAfter
);

This way Arkos allows you to define middlewares for developers to intercept the auto generated endpoints to handle app custom business logic, so that you don't lose the power of customization and not being able to do things of your own app domain.

Intercepting Requests Before And After Processing

1. Interceptor Middlewares Folder Structure

To add custom middleware, create a folder structure that follows Arkos conventions:

my-arkos-project/
└── src/
└── modules/
└── [model-name]/
└── [model-name].middlewares.ts

Create interceptors in your module's middleware file for the post model:

// src/modules/post/post.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { catchAsync } from "arkos/error-handler";

export const beforeCreateOne = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// Your logic here

next();
}
);

export const afterCreateOne = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// Your logic here

next();
}
);

See that in Arkos we've 2 groups of interceptor middlewares Before and After, which are used to intercept the auto generated request flow before and after as their names states.

2. Understanding Before Interceptor Middlewares

This kind of middlewares are used to intercept any requests before the final main handler generated by Arkos internally. Arkos allows you to intercept all the request from endpoints generated for the models.

For each model endpoints you can intercept the request before the final operation by using the following middlewares:

// src/modules/post/post.middlewares.ts

// Before operations for models
export const beforeCreateOne = catchAsync(async (req, res, next) => {});
export const beforeUpdateOne = catchAsync(async (req, res, next) => {});
export const beforeDeleteOne = catchAsync(async (req, res, next) => {});
export const beforeFindOne = catchAsync(async (req, res, next) => {});
export const beforeFindMany = catchAsync(async (req, res, next) => {});
info

For the middlewares to work you must define them under src/modules/model-name/model-name.middlewares.ts and then export the middlewares as above. You understand more about the folder structure clicking here

danger

You must pay attention on the model names so that you don't end up writing something that doesn't work. the model names on the files' name and folders' name must be in kebab-case in must be in singular. Read more about kebab-case clicking here.

If you wish to pass some data from before to after while handling the request you can attach it on req object.

Example

// src/modules/posts/post.middlewares.ts

// src/modules/author/author.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { catchAsync } from "arkos/error-handler";

export const beforeCreateOne = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// some code
(req as any).someFieldToPassToAfter = someData;
// some code
next();
}
);

export const afterFindMany = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// some code
console.log((req as any).someFieldToPassToAfter); // will log the data
// some code
next();
}
);

3. Understanding After Interceptor Middlewares

This kind of middlewares are used to intercept any requests after the final main handler generated by Arkos internally. Arkos allows you to intercept all the request from endpoints generated for the model.

1. Differences with Before intercepter middlwares: As these are middlewares that runs after the request, here arkos provides ways to access the data that would be sent normally by the final operation that ran before it or will be sent if you call next() on a After interceptor middlewares.

  • req.responseData - Allows you to access the response data that was created/modified on the final hanlder that ran before it and would be sent if there was no After interceptor middleware or if you call next() on the After intercepter middleware.
  • req.responseStatus - Allows you to access the response http status code that would be sent if there was no After interceptor middleware or if you call next() on the After intercepter middleware.
  • req.additionalData - Here Arkos provides you a way to access some additional data that was generated on the final operation and would not go anywhere in the response, for example when using authentication and there is a signup Arkos may generate a email verification otp which you will be able to acess through this.
tip

You don't need to worry too much about req.additionalData because most of the time and in most of the After Interceptor Middlewares it will just be null.

For each model endpoints you can intercept the request before the final operation by using the following middlewares:

// src/modules/post/post.middlewares.ts

// Before operations for models
export const beforeCreateOne = catchAsync(async (req, res, next) => {});
export const beforeUpdateOne = catchAsync(async (req, res, next) => {});
export const beforeDeleteOne = catchAsync(async (req, res, next) => {});
export const beforeFindOne = catchAsync(async (req, res, next) => {});
export const beforeFindMany = catchAsync(async (req, res, next) => {});

// After operations for models
export const afterCreateOne = catchAsync(async (req, res, next) => {});
export const afterUpdateOne = catchAsync(async (req, res, next) => {});
export const afterDeleteOne = catchAsync(async (req, res, next) => {});
export const afterFindOne = catchAsync(async (req, res, next) => {});
export const afterFindMany = catchAsync(async (req, res, next) => {});
info

Notice that all kind of middlewares stays at the same file as shown above, wether it is an Before or After Interceptor Middleware.

Creating Interceptor/Custom Middleware

Example middleware implementation with author model:

// src/modules/author/author.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { catchAsync } from "arkos/error-handler";

export const beforeCreateOne = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// Add custom logic before creating a author
console.log("Creating author:", req.body);
// You can modify req.body here
next();
}
);

export const afterFindMany = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// Modify response data after fetching authors
const data = req.responseData.data.map((author) => {
// Remove sensitive data or transform response
delete author.country; // Just an example
return author;
});
req.responseData.data = data;
next();
}
);

Example middleware implementation with post model:

// src/modules/author/author.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { catchAsync, AppError } from "arkos/error-handler";

export const beforeUpdateOne = catchAsync(
async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
// Custom logic to prevent an author to change other authors post
if (req.user.roles.some((role) => role.name === "Author")) {
const postToUpdate = await prisma.post.find({
where: {
id: req.query.id
author: {
id: req.user.id
}
}
})

// If the post does not belong to the author trying to update throw an error
if (!postToUpdate) throw new AppError("You can only update your own posts", 403)
}

next();
}
);
tip

catchAsync: when writing a function that must be used during a request prefer always to wrap it with catchAsync from arkos/error-handleras shown above, so that you do not need to implement your own logic for handling async or even simple functions that throws errors.

tip

AppError: when you want to throw an error you can use the AppError class, which is highly recommend, that allows you to easy setup an error with http code and other things, see more about AppError.

3. Customizing Base Query Options

Most of the time when designing your applications, you will want to customize your models base query options so that you have control over what is exposed what must not and so on, for example, hiding sensitive data like password field in all user requests or including some fields like profile as it is not included by default, by doing something like this in normal Express app:

You can read more advanced guide about customizing prisma query options.

import { prisma } from "../../utils/prisma";
import { catchAsync } from "arkos/error-handler";

export const findOneUser = catchAsync(async (req, res, next) => {
const user = await prisma.user.findUnique({
where: {
id: req.params.id,
},
include: {
password: false,
profile: true,
},
});

res.status(200).json({ data: user });
});

In Arkos yes you can do this just if you want, because Arkos was designed to make your development easy and fast mainly to avoid unecessary tasks like these ones. You can have an overview on how Arkos allows you to customize your base query options without the need to write any single controller or service.

Arkos uses what is called the prismaQueryOptions configuration, that allows you to customize how Prisma queries are executed by default in all your requests for a given model, you can even customize by methods like findOne, findMany, etc:

In order to do this you must export a modelNamePrismaQueryOptions under src/modules/model-name/model-name.prisma-query-options.ts.

Example Customizing Prisma Query Options

// src/modules/user/user.prisma-query-options.ts
import { Prisma } from "@prisma/client";
import { PrismaQueryOptions } from "arkos/prisma";

const userPrismaQueryOptions: PrismaQueryOptions<Prisma.UserDelegate> = {
queryOptions: {
// Used to manage custom prisma query options across all operations
include: {
password: false,
passwordChangedAt: false,
isActive: false,
} as any,
},
findMany: {
// Only applies for findMany operations, it will be merged with the queryOptions above
select: {
id: true,
name: true,
email: true,
password: false,
// Excluded sensitive data, etc.
},
},
createOne: {
// Only on createOne operations
include: {
postsLiked: true,
},
},
};

export default userPrismaQueryOptions;
tip

Bear in mind that the model name on the file and folder names must be singular and in kebab-case, e.g: UserProfile to user-profile.

This way you Arkos renforces one of it's biggest strength which is abstraction but still allowing the developer to customize basically everything he wants. For more you can see about advanced usage.