@tryghost/api-framework
A composable request pipeline that runs Ghost's API controllers through validation, serialization, permissions, and query stages.
Repository Health
Technical Analysis
@tryghost/api-framework is the shared request pipeline that powers every version of Ghost’s Content and Admin APIs. Instead of writing controllers as ad-hoc functions, you describe each endpoint as a declarative configuration object — allowed options, validation rules, a permissions check, and a query function — and the framework wires a Frame instance through a fixed sequence of stages: input validation, input serialization, permissions, the controller query itself, and output serialization.
The package also ships an http wrapper that adapts an Express request/response cycle into the same Frame-based pipeline, so the identical controller configuration can be invoked internally (for jobs, migrations, or one API version calling another) or externally over HTTP with no duplicated logic. Shared validators and serializers handle the common cases (pagination params, id/slug/date formats, include filtering) while still letting individual controllers override or fully replace any stage.
What You Get
- A
Frameclass that carries a request’s original input, parsed options/data, user, files, and response through every pipeline stage by reference - A
pipeline()function that turns a declarative controller config (options,validation,permissions,query,response) into a callable async controller method - An
http()wrapper that adapts Expressreq/resinto aFrameand back into a JSON, plain-text, or stream response, including cache-key generation and response caching hooks - Shared input validators (pagination, id/slug/date/email/uuid formats,
includefiltering) and a serializer pipeline with per-resourcebefore/after/allhooks and default-serializer fallback - Consistent
IncorrectUsageErrorguards that fail loudly if a controller forgets to declare apermissionskey or aqueryfunction
Common Use Cases
- Defining a new Ghost Admin or Content API resource (posts, tags, members, etc.) as a config object instead of hand-rolling Express route handlers
- Reusing the exact same controller logic for both an HTTP route and an internal call site (e.g. a migration script calling
api.posts.edit()directly) - Adding response caching to an expensive read endpoint via the
cache.get/cache.sethooks without touching the controller’s own query logic - Building a new versioned API surface that needs the same validation/serialization conventions as existing Ghost API versions
Under The Hood
Architecture
A Frame class holds all request state (original, options, data, user, file, files, response) and is passed by reference through every stage, so no stage has to return and re-thread state back into the caller. lib/pipeline.js defines a STAGES object (validation, serialisation, permissions, query) and a pipeline() function that wraps a declarative controller config into a memoized async method per docName/method pair. lib/http.js adapts an Express request/response into the same Frame and calls the identical pipeline used for in-process calls, so HTTP and internal callers share one code path. The package lives at packages/api-framework inside the TryGhost/framework pnpm/nx monorepo and depends on sibling workspace packages (@tryghost/debug, @tryghost/errors, @tryghost/promise, @tryghost/tpl, @tryghost/validator); because every Ghost API controller across every API version is built on this pipeline, changing the stage order or the Frame shape here has framework-wide blast radius.
Tech Stack
Plain JavaScript (no TypeScript in this package, though the monorepo mixes in some elsewhere), with lodash used for deep cloning, picking, and omitting request fields. Dependencies on sibling packages are resolved via pnpm workspace protocol (workspace:^) inside an Nx-managed monorepo; linting runs through oxlint, formatting through oxfmt, and releases are cut with nx release and published to the public npm registry.
Code Quality
The package has a comprehensive test suite (api-framework, frame, headers, http, pipeline, plus dedicated validator and serializer specs) built on Vitest with Sinon stubs and Node’s built-in assert/strict. Error handling is explicit and typed — stages reject with named error classes (IncorrectUsageError, BadRequestError, ValidationError) from @tryghost/errors rather than throwing generic errors or swallowing failures. Naming is consistently camelCase, public shapes are documented with JSDoc @typedefs for Frame and ControllerMethod, and oxlint runs via lint-staged on commit.
API Design
The framework’s value is architectural rather than algorithmic: controllers are pure configuration (options, validation, permissions, query) instead of imperative Express handlers, which is what lets the same controller run over HTTP or be invoked in-process with no duplicated logic. Validation and serialization rules layer from global defaults down through per-API-version and per-resource overrides, so common concerns (pagination, id/slug/date formats) are defined once and inherited everywhere, while individual endpoints can still fully opt out and handle a stage themselves.