Core ConceptsComponents

Service Hooks

Service Hooks run logic at the service layer — every time a BaseService method is called, whether from an HTTP endpoint or programmatically. Unlike interceptors (HTTP layer), hooks fire for all service calls across your entire application.

Why Service Hooks?

Interceptors run at the HTTP layer — they only fire when an API endpoint is called. But what about when you call the same service method from a background job, a CLI command, or another service? Those calls would bypass your interceptor logic. Service Hooks solve this. They run at the service layer, every time a BaseService method is called — regardless of where the call comes from.

The Three Hook Types

Before Service Hooks

Run before the main operation. Use them to:

  • Modify input data before it hits the database
  • Validate business rules that apply everywhere
  • Add default values (slug, timestamps, etc.)
export const beforeCreateOne = [
  async ({ data }) => {
    if (!data.slug && data.title) {
      data.slug = data.title.toLowerCase().replace(/\s+/g, "-");
    }
  },
];

After Service Hooks

Run after the main operation succeeds. Use them to:

  • Access the result via result
  • Send notifications, emails, or webhooks
  • Update related records
  • Trigger side effects
export const afterCreateOne = [
  async ({ result }) => {
    await emailService.sendWelcome(result.email);
  },
];

OnError Service Hooks

Run when the main operation fails. Use them to:

  • Rollback database transactions
  • Clean up temporary data
  • Log errors for monitoring
export const onCreateOneError = [
  async ({ error, data }) => {
    if (data.tempFile) await deleteFile(data.tempFile);
    logger.error(error);
  },
];

File Structure

src/modules/post/
├── post.service.ts       # Extend BaseService
└── post.hooks.ts         # Hook functions

Generate both with the CLI:

npx arkos generate service --module post
npx arkos generate hooks --module post

Or shorthand:

npx arkos g s -m post
npx arkos g h -m post

Creating a Service

Extend BaseService to add your own methods:

src/modules/post/post.service.ts
import { BaseService } from "arkos/services";

class PostService extends BaseService<"post"> {
  async getPublished() {
    return this.findMany({ where: { published: true } });
  }
}

export default new PostService("post");

Creating Hooks

Each hook exports an array of functions that run before, after, or on error of a service operation:

src/modules/post/post.hooks.ts
import { BeforeCreateOneHookArgs } from "arkos/services";
import { Prisma } from "@prisma/client";

export const beforeCreateOne = [
  async ({ data }: BeforeCreateOneHookArgs<Prisma.PostDelegate>) => {
    if (!data.slug && data.title) {
      data.slug = data.title.toLowerCase().replace(/\s+/g, "-");
    }
  },
];

Available Hooks

All available service hooks.

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
beforeCountBefore counting 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
afterCountAfter counting records
onFindOneErrorWhen fetching fails
onFindManyErrorWhen fetching fails
onCreateOneErrorWhen creation fails
onCreateManyErrorWhen creation fails
onUpdateOneErrorWhen update fails
onUpdateManyErrorWhen update fails
onDeleteOneErrorWhen deletion fails
onDeleteManyErrorWhen deletion fails
onCountErrorWhen counting fails

Hook Arguments

// Before hooks
beforeCreateOne = [
  async ({ data, context, queryOptions }) => {
    // Modify data before it hits the database
  },
];

// After hooks
afterCreateOne = [
  async ({ result, data, context, queryOptions }) => {
    // Access result after operation
    const created = result;
  },
];

// Error hooks
onCreateOneError = [
  async ({ error, data, context, queryOptions }) => {
    // Clean up when something fails
    console.error(error);
  },
];

Context contains:

  • user — Authenticated user
  • accessToken — JWT token
  • skip — Array of hook types to skip
  • throwOnError — Whether to re-throw errors

Passing Context

Pass context when calling services programmatically:

import postService from "./post.service";

// Hooks receive this context
await postService.createOne(
  { title: "New Post" },
  { include: { author: true } },  // query options
  { user: currentUser }           // context — passed to hooks
);

Skipping Hooks

Skip specific hook types when needed:

await postService.createOne(
  data,
  {},
  {
    skip: ["before", "after"],  // Skip before and after hooks
    user: currentUser,
  }
);

Example: User Registration

src/modules/user/user.hooks.ts
import { BeforeCreateOneHookArgs, AfterCreateOneHookArgs } from "arkos/services";
import { Prisma } from "@prisma/client";
import authService from "../auth/auth.service";
import emailService from "../email/email.service";

export const beforeCreateOne = [
  async ({ data }: BeforeCreateOneHookArgs<Prisma.UserDelegate>) => {
    // Hash password
    if (data.password) {
      data.password = await authService.hashPassword(data.password);
    }

    // Generate username from email
    if (!data.username && data.email) {
      data.username = data.email.split("@")[0];
    }
  },
];

export const afterCreateOne = [
  async ({ result }: AfterCreateOneHookArgs<Prisma.UserDelegate>) => {
    // Create default profile
    await prisma.profile.create({
      data: {
        userId: result.id,
        displayName: result.username,
      },
    });

    // Send welcome email (don't await — non-blocking)
    emailService.sendWelcome(result.email).catch(console.error);
  },
];

Service Hooks vs Interceptors

Service HooksInterceptors
Runs onAll service calls (API + programmatic)HTTP endpoints only
Access toService context, userFull Express req/res
Best forBusiness logic, data validationRequest processing, response formatting
File*.hooks.ts*.interceptors.ts