Tutorial: Extend Behavior with Method Hooks
This continues the blog API from Build Your First API and Add Authentication. We’ll add logging, input normalization, and timing to PostsService.create() — without changing a single line inside it. Read Method Hooks alongside this if the stacking order isn’t clear from the example alone.
1. Add the dependency
Section titled “1. Add the dependency”{ "dependencies": { "@blixis-io/method-hooks": "workspace:*" }}@blixis-io/method-hooks has no dependency on @blixis-io/core or @blixis-io/http — it works on any class, @Injectable() or not. Nothing to wire into a module; @Before/@After/@Around are just method decorators.
2. @Before — normalize and log the attempt
Section titled “2. @Before — normalize and log the attempt”import { Before } from "@blixis-io/method-hooks";// ...other imports
@Injectable()export class PostsService { @Before((input: CreatePostInput) => { console.log("creating post:", input.title); return [{ ...input, title: input.title.trim() }]; }) create(input: CreatePostInput): Post { const id = String(this.#nextId++); const post: Post = { id, title: input.title, body: input.body, createdAt: new Date().toISOString() }; this.#posts.set(id, post); return post; }
// ...list/get/update/remove unchanged}The hook returns an array ([newInput]) — that becomes create’s actual argument, so a title of " hi " reaches create as "hi". create itself never changed; it has no idea its input was trimmed before it saw it.
3. @After — shape the response
Section titled “3. @After — shape the response”import { After, Before } from "@blixis-io/method-hooks";// ...other imports
@Injectable()export class PostsService { @Before((input: CreatePostInput) => { console.log("creating post:", input.title); return [{ ...input, title: input.title.trim() }]; }) @After((result: Post) => { console.log("post created:", result.id); return result; }) create(input: CreatePostInput): Post { // ...unchanged }}@After’s hook must return the result — here it’s unchanged, just observed, but it could just as easily return a modified copy (adding a computed field, redacting something) without create knowing that happened either.
4. @Around — time it
Section titled “4. @Around — time it”import { After, Around, Before } from "@blixis-io/method-hooks";// ...other imports
@Injectable()export class PostsService { @Before((input: CreatePostInput) => { console.log("creating post:", input.title); return [{ ...input, title: input.title.trim() }]; }) @After((result: Post) => { console.log("post created:", result.id); return result; }) @Around((next: (input: CreatePostInput) => Post, input: CreatePostInput) => { const start = performance.now(); const result = next(input); console.log("create took", Math.round(performance.now() - start), "ms"); return result; }) create(input: CreatePostInput): Post { // ...unchanged }}5. Try it
Section titled “5. Try it”curl -X POST localhost:3000/posts -H 'content-type: application/json' -d '{"title": " hello world "}'Console output, in this order:
creating post: hello worldcreate took 0 mspost created: 1The first line logs the untrimmed title — @Before’s hook receives the original arguments and only replaces them for the call it makes after logging. The response body’s title is "hello world" (trimmed), since that’s what create itself actually received.
Matches Method Hooks#stacking-order: @Before (topmost) runs first; @Around (bottommost, closest to create) wraps the actual call, so its own pre/post logic sits right against the real work; @After runs last, once everything inward has returned.
6. Test the call order directly
Section titled “6. Test the call order directly”import { describe, expect, it, vi } from "vitest";import { PostsService } from "./posts.service.js";
describe("PostsService.create hooks", () => { it("trims the title before create runs, in the order before → around → after", () => { const log: string[] = []; const service = new PostsService(); vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => log.push(String(args[0])));
const result = service.create({ title: " hi ", body: "" });
expect(result.title).toBe("hi"); expect(log).toEqual(["creating post:", "create took", "post created:"]); });});No mocking of @blixis-io/method-hooks itself — this is the real PostsService, decorated exactly as it runs in production, asserting on real console output. Same “test the real thing” approach as Test-Driven API Development.
- The exact stacking-order rule these three decorators follow when combined: Method Hooks.
- Every exported symbol:
@blixis-io/method-hooksreference.