Core ConceptsComponents

Interceptors

Interceptors let you run custom logic before, after, or on error of any auto-generated endpoint — Prisma model routes, authentication routes, and file upload routes — without replacing the built-in behavior.

Interceptors work alongside Route Hook for customizing built-in routes (just like ArkosRouter) and give a complete experience when interacting with them.

Why Interceptors?

Arkos auto-generates routes for your Prisma models, authentication, and file uploads. But most applications need custom business logic — setting default values, logging activity, sending notifications, cleaning up resources on failure. Interceptors are the answer. They let you inject your own logic into the auto-generated flow without rewriting the built-in handlers.

The Three Hook Types

Before Interceptors

Run before the main operation. Use them to:

  • Modify request data (req.body, req.query, req.params)
  • Validate business rules
  • Check permissions beyond role-based access
  • Add default values
export const beforeCreateOne = [
  async (req, res, next) => {
    req.body.authorId = req.user.id;
    next();
  },
];

After Interceptors

Run after the main operation succeeds. Use them to:

  • Access the result via res.locals.data.data
  • Send notifications, emails, or webhooks
  • Log successful operations
  • Transform response data
export const afterCreateOne = [
  async (req, res, next) => {
    const record = res.locals.data.data;
    await emailService.sendWelcome(record.email);
    next();
  },
];

OnError Interceptors

Run when the main operation fails. Use them to:

  • Clean up uploaded files
  • Rollback database transactions
  • Log errors for monitoring
  • Send failure alerts
export const onCreateOneError = [
  async (err, req, res, next) => {
    if (req.file) await deleteFile(req.file.path);
    console.error(err.message);
    next(err);
  },
];

File Structure

src/modules/post/
├── post.middlewares.ts       # Reusable functions
└── post.interceptors.ts      # Chains functions to hooks

Scaffold interceptor files with the CLI:

npx arkos generate interceptors --module post
# shorthand
npx arkos g i -m post

This creates:

  • src/modules/post/post.interceptors.ts — empty hook chains

For authentication and file upload modules, use --module auth or --module file-upload.

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

export const logCreation = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  console.log("Post created:", res.locals.data.data);
  next();
};
src/modules/post/post.interceptors.ts
import { logCreation } from "./post.middlewares";

export const afterCreateOne = [logCreation];

Prisma Model Interceptors

Intercept any CRUD operation for prisma model request — add default values before create, log activity after update, or clean up files when creation fails.

src/modules/post/post.interceptors.ts
import { setAuthor, logCreation, cleanupImage } from "@/src/modules/post/post.middlewares";

export const beforeCreateOne = [setAuthor];
export const afterCreateOne = [logCreation];
export const onCreateOneError = [cleanupImage];
src/modules/post/post.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { AppError } from "arkos/error-handler";

export const setAuthor = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  req.body.authorId = req.user.id;
  next();
};

export const logCreation = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  console.log("Post created:", res.locals.data.data);
  next();
};

export const cleanupImage = async (
  err: any,
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  if (req.body.imageUrl) await deleteFile(req.body.imageUrl);
  next(err);
};

Available Prisma Model Interceptors

All before, after, and error hooks for Prisma model operations — find, create, update, delete, single or bulk.

HookTrigger
beforeFindOneBefore fetching a single record
beforeFindManyBefore fetching multiple records
beforeCreateOneBefore creating a record
beforeCreateManyBefore creating multiple records
beforeUpdateOneBefore updating a record
beforeUpdateManyBefore updating multiple records
beforeDeleteOneBefore deleting a record
beforeDeleteManyBefore deleting multiple records
afterFindOneAfter fetching a record
afterFindManyAfter fetching records
afterCreateOneAfter creating a record
afterCreateManyAfter creating multiple records
afterUpdateOneAfter updating a record
afterUpdateManyAfter updating multiple records
afterDeleteOneAfter deleting a record
afterDeleteManyAfter deleting multiple records
onFindOneErrorWhen fetching a record fails
onFindManyErrorWhen fetching records fails
onCreateOneErrorWhen creating a record fails
onCreateManyErrorWhen creating multiple records fails
onUpdateOneErrorWhen updating a record fails
onUpdateManyErrorWhen updating multiple records fails
onDeleteOneErrorWhen deleting a record fails
onDeleteManyErrorWhen deleting multiple records fails

How To Access Data In After Interceptors

After a successful operation, the result is available at res.locals.data.data:

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

export const afterCreateOne = [
  async (req: ArkosRequest, res: ArkosResponse, next: ArkosNextFunction) => {
    // The created/updated record
    const record = res.locals.data.data;

    // For list operations, it's an array
    const records = res.locals.data.data;

    // For bulk operations, metadata is also available
    const { total, results } = res.locals.data;

    next();
  },
];

Authentication Interceptors

Intercept login to rate-limit attempts, send welcome emails after signup, or log failed logins for security monitoring.

src/modules/auth/auth.interceptors.ts
import { trackLoginAttempt, sendWelcomeEmail, logFailedLogin } from "./auth.middlewares";

export const beforeLogin = [trackLoginAttempt];
export const afterSignup = [sendWelcomeEmail];
export const onLoginError = [logFailedLogin];
src/modules/auth/auth.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";

export const trackLoginAttempt = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  await redis.incr(`login:${req.body.email}`);
  next();
};

export const sendWelcomeEmail = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  const user = res.locals.data.data;
  await emailService.sendWelcome(user.email);
  next();
};

export const logFailedLogin = async (
  err: any,
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  console.error(`Failed login for ${req.body.email}: ${err.message}`);
  next(err);
};

Available Authentication Interceptors

Before, after, and error hooks for login, signup, logout, password changes, and user profile endpoints.

HookTrigger
beforeLoginBefore login
afterLoginAfter successful login
onLoginErrorWhen login fails
beforeSignupBefore signup
afterSignupAfter successful signup
onSignupErrorWhen signup fails
beforeLogoutBefore logout
afterLogoutAfter logout
onLogoutErrorWhen logout fails
beforeUpdatePasswordBefore password update
afterUpdatePasswordAfter password update
onUpdatePasswordErrorWhen password update fails
beforeGetMeBefore fetching current user
afterGetMeAfter fetching current user
onGetMeErrorWhen fetching current user fails
beforeUpdateMeBefore updating current user
afterUpdateMeAfter updating current user
onUpdateMeErrorWhen updating current user fails
beforeDeleteMeBefore deleting current user
afterDeleteMeAfter deleting current user
onDeleteMeErrorWhen deleting current user fails

File Upload Interceptors

Intercept file uploads to validate references before delete, log upload activity, or check permissions before serving files.

src/modules/file-upload/file-upload.interceptors.ts
import { checkReferences, logUpload } from "./file-upload.middlewares";

export const beforeDeleteFile = [checkReferences];
export const afterUploadFile = [logUpload];
src/modules/file-upload/file-upload.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { AppError } from "arkos/error-handler";

export const checkReferences = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  const { fileName } = req.params;
  if (await db.isReferenced(fileName)) {
    throw new AppError("File still in use", 400);
  }
  next();
};

export const logUpload = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  console.log("Uploaded:", res.locals.data.urls);
  next();
};

Available File Upload Interceptors

Before, after, and error hooks for serving, uploading, replacing, and deleting files.

HookTrigger
beforeFindFileBefore serving a file
beforeUploadFileBefore uploading a file
afterUploadFileAfter successful upload
beforeUpdateFileBefore replacing a file
afterUpdateFileAfter successful replacement
beforeDeleteFileBefore deleting a file
afterDeleteFileAfter successful deletion

Passing Data Between Interceptors

Use res.locals to pass data between interceptors in the same request:

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

export const loadOriginal = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  const post = await postService.findOne({ id: req.params.id });
  res.locals.originalPost = post;
  next();
};

export const notifyChanges = async (
  req: ArkosRequest,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  const original = res.locals.originalPost;
  const updated = res.locals.data.data;
  if (original.title !== updated.title) {
    await emailService.notify(original.authorId, "title changed");
  }
  next();
};
src/modules/post/post.interceptors.ts
import { loadOriginal, notifyChanges } from "./post.middlewares";

export const beforeUpdateOne = [loadOriginal];
export const afterUpdateOne = [notifyChanges];

Type Safety

Use ArkosRequest and ArkosResponse generics for fully typed interceptors:

src/modules/post/post.middlewares.ts
import { ArkosRequest, ArkosResponse, ArkosNextFunction } from "arkos";
import { Prisma } from "@prisma/client";
import { ArkosPrismaInput } from "arkos/prisma";

type CreatePostBody = ArkosPrismaInput<Prisma.PostCreateInput>;

export const addDefaults = async (
  req: ArkosRequest<any, any, CreatePostBody>,
  res: ArkosResponse,
  next: ArkosNextFunction
) => {
  if (!req.body.publishedAt) {
    req.body.publishedAt = new Date();
  }
  next();
};