Skip to content

Middleware

The Minions SDK provides two complementary mechanisms for adding custom logic around operations:

  • Middleware Pipeline — intercepts all Minions client operations (create, update, delete, save, load, etc.)
  • Storage Hooks — intercepts storage-level operations only (get, set, delete, list, search)

Use these for logging, authorization, auditing, caching, encryption, analytics, and more.


Middleware follows a Koa-style onion model: each middleware can run logic before and after the core operation by placing code on either side of the next() call.

mw1-before → mw2-before → core operation → mw2-after → mw1-after
import { Minions, type MinionMiddleware } from 'minions-sdk';
const logger: MinionMiddleware = async (ctx, next) => {
console.log(`${ctx.operation}`, ctx.args);
await next();
console.log(`${ctx.operation}`, ctx.result);
};
const minions = new Minions({ middleware: [logger] });

Every middleware receives a MinionContext with:

PropertyDescription
operationThe operation being intercepted (create, update, save, load, etc.)
argsOperation arguments (e.g. { typeSlug, input } for create)
resultSet by the core operation — undefined/None before next()
metadataFree-form dict for sharing state between middleware
OperationWhen it fires
createminions.create()
updateminions.update()
softDelete / soft_deleteminions.softDelete() / minions.soft_delete()
hardDelete / hard_deleteminions.hardDelete() / minions.hard_delete()
restoreminions.restore()
saveminions.save()
loadminions.load()
removeminions.remove()
listminions.listMinions() / minions.list_minions()
searchminions.searchMinions() / minions.search_minions()

Skip next() to prevent the core operation from executing. Set ctx.result manually if the caller expects a return value:

const authGuard: MinionMiddleware = async (ctx, next) => {
if (!ctx.metadata.userId) {
throw new Error('Unauthorized');
}
await next();
};
const cache: MinionMiddleware = async (ctx, next) => {
if (ctx.operation === 'load') {
const cached = myCache.get(ctx.args.id as string);
if (cached) {
ctx.result = cached; // skip storage entirely
return;
}
}
await next();
};

Use ctx.metadata to pass data between middleware:

const addUserId: MinionMiddleware = async (ctx, next) => {
ctx.metadata.userId = getCurrentUser();
await next();
};
const auditLog: MinionMiddleware = async (ctx, next) => {
await next();
await recordAudit(ctx.operation, ctx.metadata.userId, ctx.result);
};
const minions = new Minions({ middleware: [addUserId, auditLog] });

For storage-only concerns, withHooks wraps any StorageAdapter with before/after callbacks — no changes to the Minions client needed.

import { withHooks, MemoryStorageAdapter, Minions } from 'minions-sdk';
const storage = withHooks(new MemoryStorageAdapter(), {
beforeSet: async (minion) => {
console.log('Saving:', minion.title);
return minion; // return transformed minion, or void
},
afterGet: async (id, result) => {
if (result) metrics.increment('reads');
},
});
const minions = new Minions({ storage });
HookSignatureNotes
beforeGet(id) → voidFires before get()
afterGet(id, result) → voidFires after get()
beforeSet(minion) → Minion?Can return a transformed minion
afterSet(minion) → voidFires after set()
beforeDelete(id) → voidFires before delete()
afterDelete(id) → voidFires after delete()
beforeList(filter?) → voidFires before list()
afterList(results, filter?) → voidFires after list()
beforeSearch(query) → voidFires before search()
afterSearch(results, query) → voidFires after search()

beforeSet can return a new minion to replace the original before storage:

const storage = withHooks(innerAdapter, {
beforeSet: async (minion) => ({
...minion,
fields: { ...minion.fields, savedAt: new Date().toISOString() },
}),
});

Choosing Between Middleware and Storage Hooks

Section titled “Choosing Between Middleware and Storage Hooks”
MiddlewareStorage Hooks
ScopeAll client operationsStorage operations only
Short-circuit✅ Skip next()❌ Not supported
Transform dataVia ctx.resultVia beforeSet return
Order-aware✅ Onion model✅ Sequential
No client changesNeeds middleware configJust wrap the adapter

[!TIP] Use middleware for cross-cutting logic across all operations (auth, logging, metrics). Use storage hooks for storage-specific concerns (encryption, caching, auditing writes).