Skip to content

Hooks & Middleware

Lifecycle hooks

Hooks fire around each mutating operation. They receive the data and a context object with modelName.

ts
hiroki.importModel(User, {
  hooks: {
    beforeCreate: async (body, ctx) => {
      // mutate and return the body
      return { ...body, createdAt: new Date() };
    },
    afterCreate: async (doc, ctx) => {
      await auditLog.record('create', ctx.modelName, doc);
    },
    beforeUpdate: async (body, ctx) => {
      return { ...body, updatedAt: new Date() };
    },
    afterUpdate: async (doc, ctx) => { ... },
    beforeDelete: async (id, ctx) => {
      // throw to cancel the delete
      if (await hasRelations(id)) throw new Error('Has relations');
    },
    afterDelete: async (doc, ctx) => { ... },
  },
});

Hook signatures

HookSignatureNotes
beforeCreate(body, ctx) => body | Promise<body>return mutated body
afterCreate(doc, ctx) => void | Promise<void>
beforeUpdate(body, ctx) => body | Promise<body>return mutated body
afterUpdate(doc, ctx) => void | Promise<void>
beforeDelete(id, ctx) => void | Promise<void>throw to cancel
afterDelete(doc, ctx) => void | Promise<void>

ctx is { modelName: string }.

Middleware

Middleware runs around the entire request dispatch — before routing to get/post/put/delete. Useful for auth guards, rate limiting, and request logging.

ts
const authMiddleware: HirokiMiddleware = async (ctx, next) => {
  if (!ctx.headers?.authorization) {
    throw new Error('Unauthorized');   // returns 500; use an HttpError for clean status
  }
  return next();
};

hiroki.importModel(User, {
  middleware: [authMiddleware],
});

ctx has { path, method, body, query }.

Auth guard example

ts
import { BadRequestError } from 'hiroki';

const requireAuth: HirokiMiddleware = async (ctx, next) => {
  const token = ctx.query?.token as string | undefined;
  if (!token || !isValid(token)) {
    throw new BadRequestError('Unauthorized', 'UNAUTHORIZED');
  }
  return next();
};

Middleware chain

Multiple middleware stack in order — each must call next() to continue:

ts
hiroki.importModel(User, {
  middleware: [logRequest, requireAuth, checkRateLimit],
});

If any middleware throws or returns without calling next(), the chain stops.

Rate limiting (via middleware)

Hiroki doesn't ship a rate limiter, but middleware makes it trivial to add one:

ts
const requestCounts = new Map<string, number>();

const rateLimit: HirokiMiddleware = async (ctx, next) => {
  const key = ctx.path;
  const count = (requestCounts.get(key) ?? 0) + 1;
  requestCounts.set(key, count);
  if (count > 100) throw new Error('Rate limit exceeded');
  return next();
};

Combining hooks and middleware

ts
hiroki.importModel(Order, {
  allowedFields: ['items', 'note'],       // security
  middleware: [requireAuth],              // auth before any operation
  hooks: {
    beforeCreate: async (body, ctx) => ({
      ...body,
      userId: getCurrentUserId(),         // inject server-side fields
    }),
    afterCreate: async (doc, ctx) => {
      await notifyFulfillment(doc);       // side effects after create
    },
  },
});

Released under the MIT License.