Plugin Authoring โ
ng-openapi plugins are generator classes that run after the core type/service generation and emit additional files into the same output directory. The built-in HttpResourcePlugin and ZodPlugin are implemented against the exact contract described here โ a third-party plugin needs nothing beyond the public ng-openapi API.
The contract โ
A plugin is a class implementing IPluginGenerator, constructed by the orchestrator with a single PluginGeneratorContext argument:
typescript
import { IPluginGenerator, PluginGeneratorContext } from "ng-openapi";
import * as path from "path";
export class MyPlugin implements IPluginGenerator {
private readonly context: PluginGeneratorContext;
constructor(context: PluginGeneratorContext) {
this.context = context;
}
async generate(outputRoot: string): Promise<void> {
const { spec, project, onWarning } = this.context;
if (spec.operations.length === 0) {
onWarning?.("Nothing to generate: the specification has no operations");
return;
}
const file = project.createSourceFile(path.join(outputRoot, "my-plugin", "index.ts"), "", {
overwrite: true,
});
// ... build the file from spec.operations / spec.definitions ...
file.formatText();
file.saveSync();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Users register the class in their config:
typescript
export default {
// ...
plugins: [MyPlugin],
} as GeneratorConfig;1
2
3
4
2
3
4
What the context provides โ
| Field | Type | Notes |
|---|---|---|
spec | NormalizedSpec | The version-free spec model. $refs are resolved and per-operation fields (pathParams, queryParams, hasBody, isMultipart, responseType, โฆ) are precomputed. Plugins never see Swagger 2.0 vs OpenAPI 3.x differences. |
project | Project (ts-morph) | The shared project every generator emits through. Create files via project.createSourceFile(...) so the orchestrator can report them in GenerationResult.filesWritten. |
config | GeneratorConfig | The full user-facing config. Read only the slice you need (e.g. config.clientName, config.options.dateType). |
onWarning | (message: string) => void (optional) | Sink for non-fatal diagnostics. Never console.* from a plugin โ warnings surface on GenerationResult.warnings and through the CLI's reporter. |
Rules of engagement โ
- Consume
NormalizedSpec, not the raw spec. All version quirks are resolved at parse time; if something you need is missing from the model, that is a gap to raise upstream, not a reason to re-parse the input. - Never log. The core is silent by design; the CLI owns presentation. Report problems through
onWarningor by throwing anError(which aborts generation with a clean CLI message). - Emit through the shared
project. Files written behind its back won't be tracked, formatted consistently, or visible tofixMissingImports(). - Barrels are re-exported for you. If your plugin writes an
index.tsinto its own directory directly under the output root (as in the example above), the generated rootindex.tsautomatically re-exports that directory โ users can import your symbols from the client entrypoint. Three constraints: the directory must be a direct child of the output root (nested barrels are ignored), it must contain anindex.ts, and it must not be namedmodels,providers,services,tokens, orutilsโ those are reserved for the core generators and are skipped silently. This applies only to files emitted through the sharedproject; anything written straight to disk is invisible to the scan. Prefix your exported symbols distinctively, too: the root barrel re-exports plugin directories withexport *alongside./models, and a name exported by both is silently dropped rather than reported. - Validation is done for you. By the time a plugin is constructed, the spec has been parsed, validated, and normalized โ no need for your own guards.