Skip to content

Writing API Docs ​

Our API Docs are compiled from TSDoc comments in the source code and from each package's src/index.md. For where API docs fit alongside guides and other documentation, see the Writing Documentation overview.

Of the audiences our documentation serves, API docs are read by existing users who already know what they want and need the details, and by readers who followed a link from a guide. Assume they use WarpDrive; do not assume they know the concept behind the symbol, so link the guide that teaches it on first mention. LLMs and coding agents are the third reader, and they usually read the comment in the source rather than the rendered page, where the status badges and automatic MDN links described below do not exist. Write so the words alone carry the meaning: a @deprecated comment names its replacement in text, not only by badge.

API Documentation Infra Overview ​

API Documentation is generated from TSDoc comments in the source code compiled with TypeDoc and transformed for Vitepress using typedoc-plugin-markdown.

TSDoc looks like JSDoc, but the grammar is TSDoc and the parser is TypeDoc, and the differences matter: types come from the TypeScript signature rather than from {type} annotations in tags, and TypeDoc adds tags JSDoc does not have (@since, @internal, @group, @category, and the repo-specific ones described below).

TypeDoc is configured to follow our public package entrypoints (the exports in each package's package.json) to auto-discover documentation. It documents everything reachable, public or private, including properties and methods that have no associated code docs. It uses TypeScript to understand the source-code and builds documentation from the combination of Type signatures and TSDoc comments.

This is great, but it means that its very easy to leak private APIs into the docs.

Avoid leaking private APIs into the public docs

Use /** @internal */ on anything that should not appear in the public docs.

While API Documentation lives with the source-code, the code's behavior plays no part in the documentation that is generated: everything is compiled from type signatures and comments.

The below guide will walk through best practices for writing doc comments, important nuances and syntaxes to know, as well as how to preview the doc comments.

A Complete Doc Comment ​

Everything the rest of this page asks for, on one public function:

ts
/**
 * Adds two numbers.
 *
 * @example
 * ```ts
 * add(1, 2); // 3
 * ```
 *
 * @param a - the first number to add
 * @param b - the second number to add
 * @return the sum of the two numbers
 * @since 5.10.0
 * @public
 */
export function add(a: number, b: number): number {}

Content Standards ​

Give Each API Page a @summary ​

llms.txt, the index coding agents read to decide which page to fetch, has one entry per API page (see Frontmatter and Agent-Only Content). The docs site takes that entry's description from a @summary tag, exactly as written; a page without one is listed by name only, and nothing is guessed from the comment's prose. So add a @summary, one sentence saying what the symbol does or is for, to the comment that owns a page:

  • an exported class, function, interface, type alias, variable, or enum, each of which gets its own page
  • a @module comment, which owns that module's page

Members don't need one. A method, property, or accessor renders on its parent's page, so its @summary is never read.

ts
/**
 * ## Import
 *
 * ```js
 * import { RequestManager } from '@warp-drive/core';
 * ```
 *
 * For complete usage guide see the [RequestManager Documentation](/guides/).
 *
 * @summary Runs each request through a chain of handlers that can fulfill, modify, or pass it along, and returns a `Future` for the response.
 * @public
 */
  • Write it for a reader who sees nothing else: name the symbol's purpose rather than restating its name, and don't lean on the prose around it.
  • Keep it to one sentence, under about 200 characters. A multi-line @summary is joined into one line.
  • Inline code and {@link} are fine; {@link Store} contributes its link text. Avoid other markdown and HTML entities, which reach llms.txt as literal characters.
  • @summary is not rendered on the page, so it doesn't repeat the prose readers see. It is a TypeDoc tag, not a TSDoc one; TSDoc treats everything before @remarks as the summary and has no @summary.
  • For an overloaded function, put it on the implementation's comment or the first overload's.
  • A package landing page built from src/index.md has no doc comment, so it can't carry one. Its entry comes from the description in the package's package.json instead; see README vs src/index.md.

Every Public API Should Have a Usage Example ​

Even a minimal example dramatically shortens the time it takes for a consumer to understand how to use an API. Every doc comment for a @public export should include at least one, under the @example tag. Headings inside a comment body are fine for other structure, as the markdown examples later on this page show, but the usage example itself goes under @example, which is the convention in this repo. This applies to the exported symbol itself: a class, function, or variable. A class member needs its own example only when using it is not obvious from the class example, such as a method with several call patterns.

ts
/**
 * Adds two numbers
 *
 * @example
 * ```ts
 * add(1, 2); // 3
 * ```
 *
 * @param a - the first number to add
 * @param b - the second number to add
 * @return the sum of the two numbers
 * @public
 */
function add(a: number, b: number): number {}

The first time a doc comment mentions another documented, public token, that mention should be a {@link} to it. This turns our docs into a web that's easy to navigate instead of a pile of disconnected pages. Write {@link Symbol} to show the symbol's own name, or {@link Symbol | other text} to show other text; the | separates the target from the label.

ts
/**
 * Updates the {@link User.name | User's name} with the provided
 * value.
 *
 * @public
 */
function updateUserName(user: User, name: string): void {}

When a guide teaches the concept behind a symbol, link that guide from the symbol's doc comment, usually in the paragraph after the summary, with a root-relative URL such as [Request Builders](/guides/the-manual/requests/builders). Readers who land on an API page from a search are the ones who most need the concept. The link runs both ways: a guide links the API pages it names, as Linking to the API Docs describes, and each package's landing page lists its guides, as README vs src/index.md describes. Link only published guides; drafts are hidden from the sidebar and left out of llms.txt.

Union types and object-as-enum types should link each of their members so that readers can jump directly to whichever member is relevant to them.

ts
/**
 * See also:
 * - {@link PendingRequest}
 * - {@link ResolvedRequest}
 * - {@link RejectedRequest}
 * - {@link CancelledRequest}
 */
export type RequestState<RT = unknown, E extends Error = Error> =
  PendingRequest | ResolvedRequest<RT> | RejectedRequest<RT, E> | CancelledRequest<RT, E>;

Cross-linking between the members of the same class, interface, or object is highly encouraged. This makes it fast and easy to navigate around the documentation for that class/object/interface. The @privateRemarks blocks in the example below are notes for people reading the source; TypeDoc excludes that tag from the rendered docs by default.

ts
/**
 * Additional properties exposed on errors thrown by the
 * {@link Fetch | Fetch Handler}.
 *
 * In the case of an Abort or system/browser level issue,
 * this extends {@link DOMException}.
 *
 * Else it extends from {@link AggregateError} if the
 * response includes an array of errors, falling back
 * to {@link Error} as its base.
 */
export interface FetchError extends DOMException {
  /**
   * Alias for {@link FetchError.status | status}.
   *
   * @privateRemarks
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status)
   */
  code: number;

  /**
   * The name associated to the {@link FetchError.status | status code}.
   *
   * Typically this will be of the formula `StatusTextError` for instance
   * a 404 status with status text of `Not Found` would have the name
   * `NotFoundError`.
   */
  name: string;

  /**
   * The http status code associated to the returned error.
   *
   * Browser/System level network errors will often have an error code of `0` or `5`.
   * Aborted requests will have an error code of `20`.
   *
   * @privateRemarks
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/status)
   */
  status: number;

  /**
   * The Status Text associated to the {@link FetchError.status | status code}
   * for the error.
   *
   * @privateRemarks
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response/statusText)
   */
  statusText: string;

  /**
   * A property signifying that an Error uses the {@link FetchError}
   * interface.
   */
  isRequestError: true;
}

Linking Symbols You Haven't Imported ​

{@link} can only resolve to symbols that TypeDoc can see referenced in the file. To link a symbol you otherwise have no need to import, import it as a type and suppress the resulting lint error:

ts
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Thing } from './thing.ts';
import { functionThatReturnsThing } from './thing.ts';

/**
 * Does stuff and returns a {@link Thing}
 */
function doStuff() {
  return functionThatReturnsThing();
}

NOTE

We would like to improve the @typescript-eslint/no-unused-vars lint rule (or its config) so that type-only imports which exist solely for documentation linking purposes don't trigger this error. If you'd like to help investigate this, reach out!

Linking a Module ​

Link an entire module (for instance, to point readers at a set of related flags or utilities) by importing it as a type namespace:

ts
import type * as FEATURES from './features.ts';

/**
 * see {@link FEATURES | features} for the available flags.
 *
 * @public
 */

Linking to MDN ​

typedoc-plugin-mdn-links automatically links the appropriate MDN page for web-platform types (Response, AbortController, etc.) in published documentation, so you don't need to (and shouldn't) manually link these in the main body of a doc comment.

It can still be useful to include the MDN reference manually via @privateRemarks for ease of navigation while reading the source:

ts
/**
 * Cancel this request by firing the {@link AbortController}'s signal.
 *
 * @privateRemarks
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)
 *
 * @param reason - optional reason for aborting the request
 * @public
 */

The correct link to use can usually be found by jumping to the TypeScript typedef for the token, which itself is documented with the canonical MDN reference. For instance, this is (an excerpt of) TypeScript's own type for AbortController:

ts
/**
 * A controller object that allows you to abort one or more DOM requests as and when desired.
 *
 * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController)
 */
interface AbortController {
  /**
   * Returns the AbortSignal object associated with this object.
   *
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/signal)
   */
  readonly signal: AbortSignal;

  /**
   * Invoking this method will set this object's AbortSignal's aborted flag and signal to any observers that the associated activity is to be aborted.
   *
   * [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortController/abort)
   */
  abort(reason?: any): void;
}

The @deprecated Tag ​

@deprecated is a standard TSDoc tag. In this repo it carries one extra meaning on top of the standard one: the tagged function, component, or object is actively being removed, not merely out of favor (that is @discouraged, below).

  • Whenever possible, link the replacement, and tag that replacement @recommended.
  • If the deprecation is tracked by the deprecations guide (i.e. it has a deprecation id like ember-data:deprecate-store-extends-ember-object and a corresponding DEPRECATE_* flag), link the guide and name the deprecation id so readers can find its entry there.
ts
/**
 * @deprecated use {@link ResourceKey} instead
 */
export type StableRecordIdentifier<T extends string = string> = ResourceKey<T>;

A glowing red badge is automatically added next to the heading of whatever the tag describes; the deprecation message itself stays in the page body as its own #### Deprecated section rather than being absorbed into the badge, since it's usually worth reading in full.

The @discouraged Tag ​

@discouraged signals that a function, object, or class is an older, but not-yet-deprecated, way of doing something. Unlike @deprecated, something tagged @discouraged may still have valid use cases; it's optional to describe those cases in the tag.

If a @recommended alternative exists, cross-link to it.

A glowing orange badge is automatically added next to the heading of whatever the tag describes.

@recommended is a companion tag to @deprecated and @discouraged. It marks a function, object, or class as the preferred way of doing something. It's not mandatory to link back to the @discouraged or @deprecated alternative(s), but doing so is encouraged. Since @recommended implies the existence of a @discouraged and/or @deprecated counterpart, it shouldn't be used on its own.

A glowing blue/green badge is automatically added next to the heading of whatever the tag describes.

The @legacy Tag, and the @warp-drive/legacy and @warp-drive/experiments Badges ​

@legacy is a repo-specific modifier tag for symbols that predate the current architecture but aren't (yet) @deprecated — it renders as a plain, unstyled **`Legacy`** flag before the summary, the same way any other modifier tag without special handling does (see @discouraged/@recommended above for tags that do get special handling).

Separate from that tag, every page under the @warp-drive/legacy package automatically gets a red "@legacy" badge next to its <ModuleBadge>, and every page under @warp-drive/experiments automatically gets an orange "@experimental" badge the same way. Both are applied purely by which package a file lives in — via docs-viewer/src/site-utils.ts checking the generated file's path — not by any doc comment tag, so nothing needs to be written in source to get them; anything moved into (or out of) either package picks up (or loses) its badge automatically.

Note that TypeDoc's own standard @experimental modifier tag (along with @alpha/@beta/@internal/@public) is unrelated to the @warp-drive/experiments badge above — tagging a symbol @experimental elsewhere in the codebase does not add that badge, and currently renders as nothing more than an unstyled **`Experimental`** flag, same as @legacy above.

Documentation Syntax ​

What are Doc Comments ​

Only /** comments are compiled as potential documentation, e.g.

ts
/**
 * This is a potential documentation block
 */

Whereas single star comment blocks are not considered documentation

ts
/*
 * This is not a potential documentation block
 */

Where to put Doc Comments ​

Documentation comments should be placed directly above the symbol they are documenting.

ts
/**
 * Documents the class
 */
class Foo {
  /**
   * Documents the method
   */
  bar() {}

  /**
   * Documents the property
   */
  bar = '1';
}

/**
 * Documents the interface
 */
interface Foo {
  /**
   * Documents the key
   */
  bar: string;
}

/**
 * Documents the type
 */
type Foo = {
  /**
   * Documents the key
   */
  bar: string;
};

/**
 * Documents the variable
 */
const Foo = '1';

/**
 * Documents the function
 */
function foo() {}

Ignored Doc Comments ​

When compiling the API documentation, any symbol whose doc comment carries the @internal tag is left out of the published docs entirely, along with everything nested under it (TypeDoc runs with excludeInternal). This is how to document code for fellow developers without exposing it to end consumers. For example, the function below would not appear in the docs:

@internal also removes the declaration from the package's types

@warp-drive/core compiles with stripInternal, so TypeScript drops every @internal declaration from the emitted .d.ts as well. Tagging a symbol that another workspace package imports, which includes anything exported from a -private.ts barrel, breaks that package's declaration build with a MISSING_EXPORT error. Use @internal on class members and on symbols nothing else imports. For a symbol that must stay importable but out of the docs, leave it in a -private module: each package's typedoc.config.mjs excludes -private entry points, so nothing there is documented unless a public entry re-exports it.

ts
/**
 * This is a private utility for updating the state
 * of a relationship.
 *
 * @internal
 */
function somethingInside() {}

Auto Association ​

TSDoc and TypeDoc will automatically place the documentation for a method inside the class it is on, the class inside the package it is in and at the export path it is exported from. Because it knows our entrypoints and our types, we don't need to tell it much! It already knows when something is an interface vs a class, when it extends something else, or that it implements a specific signature.

This means you no longer need to add redundant tags like @class @method @static and @property. The one structural tag still in use is @module, for package and subpackage overviews (see below).

Doc Comments can be Markdown ​

Doc comments can contain most any valid markdown syntax, most markdown-valid html, and can utilize code-highlighting via language prefix on a code block comment.

For instance

ts
/**
 * ## Overview
 *
 * Some details
 *
 * ### An Example
 *
 * ```ts
 * new Store();
 * ```
 *
 * @public
 */

TIP

The fence around the example above uses four backticks (````) instead of three so the nested ```ts block doesn't close it early — a closing fence must be at least as long as the opening one, per the CommonMark rule for fenced code blocks. This is plain markdown, not a TSDoc feature: our docs render through VitePress's markdown-it, which implements the same rule. Use the same trick any time an example needs a code block inside another code block.

Additionally, the markdown parser in use by our docs understands code groups, and many other features.

This means we can do code examples that toggle between files or formats.

ts
/**
 * ::: code-group
 *
 * ```ts [example.ts]
 * export function numberFromString(str: string): number {}
 * ```
 *
 * ```js [example.js]
 * export function numberFromString(str) {}
 * ```
 *
 * :::
 */

Line highlighting, focus, and code groups are three VitePress features that combine to enable crafting powerful examples in the documentation.

Doc Comments should start every line with a * ​

While technically doc comments only need to start with /**, providing a * for every line with matching indentation ensures correct parsing of all tags and documentation.

Without this, some decorators in code examples may be incorrectly parsed as documentation tags, and some documentation may be unexpectedly truncated.

Good

ts
/**
 * ## Overview
 *
 * Some details
 *
 * ### An Example
 *
 * ```ts
 * class User extends Model {
 *   @attr name;
 * }
 * ```
 *
 * @public
 */

Bad

ts
/**
 ## Overview

 Some details

 ### An Example

 ```ts
 class User extends Model {
   @attr name;
 }
 ```

 @public
*/

Documenting Packages and Subpackages ​

A package's documentation has three entry points, each its own file with its own purpose and audience:

  • The README is what npm and GitHub show. It is for someone deciding whether to install the package, and someone who just did and wants the first thing to type. Its rules are under READMEs and src/index.md below.
  • The package landing page, src/index.md, renders at /api/<package>/ in the API docs. It is for an existing user who landed on the package and wants to know where to start, so it holds the setup and the map of the package's code entry points.
  • Subpath entry points such as @warp-drive/core/request render at /api/<package>/<path>/, one page per entry file the package's exports map points at (src/request.ts for that one). They are for an existing user who already knows which part of the package they need.

The README and the landing page share an elevator-pitch snippet on purpose; everything else belongs to exactly one of the three. The rest of this section covers the two in the API docs.

Subpath entry points such as @warp-drive/core/request or @warp-drive/utilities/string get their overview from a doc comment at the top of the entry file with the tag @module. The prose in that comment renders at the top of the subpath's page, above its member listing:

ts
/**
 * Utilities for building request URLs and query strings.
 *
 * @module
 */

The package root src/index.ts is different. Its doc comment carries no prose, only tags: these two, plus a module-level @since where one applies (see the end of this section). Every package has this comment. <project> is typed literally; it is TypeDoc's name for the package root:

ts
/**
 * @module
 * @mergeModuleWith <project>
 */

The landing prose for the package lives in src/index.md instead, and the package's typedoc.config.mjs names it with readme: 'src/index.md' (see README vs src/index.md).

The reason is how TypeDoc's packages entry point strategy treats a package with more than one entry point: src/index.ts becomes a sub-module named index, with its own page at /api/<package>/index/. The package's landing page at /api/<package>/ then has no intro at all, and whatever prose the @module comment held is reachable only from that sub-page. @mergeModuleWith <project> folds the index module's exports into the package root and removes the index module, its comment included, which is why the prose cannot live there. TypeDoc renders the readme file at the top of the package's landing page, so that is where the prose goes. A package with little to say still carries both; its src/index.md holds at least the package name as an H1 and one sentence on what the package is, so that adding more later is a one-file change.

A package whose typedoc.config.mjs points TypeDoc at dist/*.d.ts rather than src (because its source has files TypeDoc cannot parse, such as the .gts components in @warp-drive/ember) loses the @module / @mergeModuleWith comment when the d.ts files are bundled. Re-add it to the root index.d.ts with the banner option in the package's tsdown.config.mjs; warp-drive-packages/ember/tsdown.config.mjs shows how. readme: 'src/index.md' works unchanged for such a package.

The one tag that may join the two above is a module-level @since, when the package as a whole first shipped in a known version. docs-viewer/src/typedoc-since-plugin.mjs reads it from that comment and renders it as a badge on the landing page.

Mark public exports with @public ​

TSDoc's release tags say who a symbol is for. In this repo every export intended for consumers carries @public; @alpha and @beta are not used. The rule applies to symbols reachable from a public entry point: an export reachable from one with no tag is published as if it were public, so give it @public or move it out of the public surface. Symbols exported only from -private modules need no tag, because TypeDoc never sees those entry points, and must not get @internal if another package imports them (see Ignored Doc Comments for why). Members of a @public class or interface inherit its visibility and do not need their own @public; use @internal on a member to hide just that member. The Content Standards above apply to anything public.

Always specify @since on non-type public APIs ​

@since names the release the API first shipped in, using the full stable version (5.9.0, not 5.9 and not a prerelease tag). For an API you are adding now, that is the next stable release. It renders as a small badge rather than a body section, so it's always visible next to the name of the thing it describes without taking up page space. The other examples on this page leave it out to stay short; real public APIs must not.

On a function, class, or variable it shows up right next to that page's own name (an interface or type alias renders it the same way if you add one, but types don't require it):

ts
/**
 * @since 1.13.0
 * @public
 */

On a package's @module doc comment, it shows up next to that package's <ModuleBadge> (the pill showing the module path at the top of the module's index page) instead:

ts
/**
 * This package provides essential types and symbols used
 * by all the other WarpDrive packages.
 *
 * @module
 * @since 5.9.0
 */

NOTE

Module-level @since is currently only picked up from a package's own root entry file (e.g. packages/core-types/src/index.ts), not from a subpackage's own @module comment (e.g. core-types/src/cache.ts). Put @since on a subpackage's exported members instead until this is extended.

On a class's own member (a method, property, etc.), @since shows up on its own line directly below the member's heading instead of next to it — a nested heading is much narrower than a page's own title, so crowding it with an inline badge reads worse much sooner.

Overriding the displayed kind and name with @badge and @title ​

Every function, class, interface, variable, type alias, and enumeration gets its own page whose heading shows its TypeScript kind (Function, Class, Interface, Variable, Type Alias, Enumeration) as a badge next to its name. That default is accurate but not always the most useful label for a reader — a class can be a component, a plain object can be a request handler, a function can be a request builder.

@badge and @title are repo-specific tags, implemented in docs-viewer/src/site-utils.ts. Use @badge <Label> to override the kind badge with a more meaningful conceptual label:

ts
/**
 * The `<Await />` component allows you to utilize reactive control flow
 * for asynchronous states in your application.
 *
 * @badge Component
 */
export class Await<T, E> extends Component<AwaitSignature<T, E>> {}
ts
/**
 * A basic Fetch Handler which converts a request into a
 * `fetch` call presuming the response to be `json`.
 *
 * @badge Handler
 */
const Fetch = { ... };

Use @title <Text> to override the name shown in the page heading itself, when the raw name (or its generic signature) isn't the most legible way to present it — for instance showing a component by its usage syntax instead of its class signature:

ts
/**
 * @badge Component
 * @title <Await />
 */
export class Await<T, E> extends Component<AwaitSignature<T, E>> {}

renders as <Await /> instead of Await<T, E>. Write the title exactly as it should be read — no escaping needed, even for angle brackets.

Both tags only affect the page's own top-level symbol; they have no effect when used on a nested class member, since only the page's own heading shows these badges.

Organizing a page with @group and @category ​

TypeDoc supports two independent, standard (not repo-specific) tags for organizing the children listed on a class's or module's own page. They look similar but behave very differently — pick whichever fits what you're organizing.

@group <Name> re-buckets what "kind" a symbol counts as. Every symbol already has a default group — the plural of its TypeScript kind (Functions, Classes, Properties, etc.) — so tagging a symbol with @group <Name> just moves it into a different, named bucket instead. Nothing else on the page is affected: every other symbol keeps whatever group it already had, explicit or default.

ts
class RequestManager {
  /**
   * @group Handlers
   */
  use(handlers: Handler[]): void {}
}

@category <Name> arranges children by topic instead of by kind — useful when several different kinds of things (a property, a function, a class) together make up one logical feature and should be presented as a unit regardless of their TypeScript kind. The tradeoff: @category is all-or-nothing per page. The moment any symbol on a page has a @category, every symbol without one falls into a generic "Other" bucket instead — there's no partial opt-in.

Given that tradeoff, default to @group, and reach for @category only when the grouping you want genuinely cuts across kinds, and you're prepared to categorize everything relevant on that page rather than just a few symbols. Once any symbol on a page has a @category, give every symbol on that page one; they may keep a @group as well, since the two tags answer different questions — see @decorator and @classDecorator below for a concrete example of this exact tradeoff.

Tag each member with the category it belongs to:

ts
class JSONAPICache {
  /**
   * @category Cache Management
   */
  mutate(mutation: Mutation): void {}

  /**
   * @category Cache Forking
   */
  fork(): Promise<JSONAPICache> {}
}

which renders as ## Cache Management and ## Cache Forking sections on JSONAPICache's page, each listing the members tagged into it. Optionally add @categoryDescription <Name> to the containing class, interface, or module's own doc comment to show a short blurb under that section's heading:

ts
/**
 * @categoryDescription Cache Management
 * APIs for primary cache management functionality
 */
class JSONAPICache {}

Keep category names short, human-scannable topic phrases consistent with existing ones in the same package (e.g. Resource Data, Resource Lifecycle, Cache Management) rather than inventing a new one-off name per symbol.

Marking decorators with @decorator and @classDecorator ​

@decorator and @classDecorator are repo-specific modifier tags (implemented by docs-viewer/typedoc-plugins/decorator-groups.mjs) for functions used as a property/field decorator or a class decorator, respectively:

ts
/**
 * @since 5.9.0
 * @public
 * @decorator
 */
export function attribute(target: object, key: string): void;
ts
/**
 * @since 5.9.0
 * @public
 * @classDecorator
 */
export function Resource(target: AnyConstructor): void;

Each tag fills in two defaults, as long as the symbol doesn't already set its own:

  • @group — "Field Decorators" for @decorator, "Class Decorators" for @classDecorator — so decorators get their own section on the module's index page instead of a flat list mixed in with everything else. This is @group rather than @category on purpose (see above): decorators live in modules (like schema-dsl) that don't otherwise use @category, so @group avoids ever tripping the "one @category recategorizes the whole page" rule. If a decorator's module later needs @category for something else, either give the decorators an explicit @category too, or keep @category out of that module entirely — don't let the two mix silently.
  • @badge — "Decorator" / "Class Decorator" — shown as the kind badge on the symbol's own page instead of the default "Function".

A symbol can still set its own @group to opt out of the default bucket, same as any other @group usage — this is how schema-dsl's entity-level decorators (Resource, Trait, ObjectSchema, trait) end up grouped under their own Entity Decorators section instead of Class Decorators:

ts
/**
 * @since 5.9.0
 * @public
 * @classDecorator
 * @group Entity Decorators
 */
export function Resource(target: AnyConstructor): void;

Use @hideconstructor for classes that aren't directly instantiated by users ​

@hideconstructor removes the constructor from the class's page. Use it on classes the Store or a manager creates for the user, so the docs don't suggest calling new on them:

ts
/**
 * @hideconstructor
 */
class ReactiveResource {}

Don't document types in @param and @return ​

Because types are parsed from the TypeScript, @param and @return should be used to give a meaningful description only. This repo uses @return; TypeDoc also accepts TSDoc's standard @returns.

ts
/**
 * Adds two numbers
 *
 * @param a - the first number to add
 * @param b - the second number to add
 * @return the sum of the two numbers
 */
function add(a: number, b: number): number {}

READMEs and src/index.md ​

Every published package has a README.md next to its package.json. It is the first thing a reader sees on the package's GitHub directory and on its npm page, and package.json lists it in files so it ships in the tarball. Its job is to say what the package is, whether the reader wants it, and where the real documentation lives. It is not the place to teach the package.

Of the audiences our documentation serves, a README is for two readers: a technical evaluator or hobbyist on npm or GitHub deciding whether to install this package at all, and someone who just installed it and needs to know the first thing to do. Neither is reading the docs site yet, so the README has to stand on its own, and neither wants depth, so it has to hand them off quickly.

README structure ​

A package README has these sections, in this order. The packages named in parentheses are the ones the shape is taken from; copy from them.

  • Logo block. A centered <img> of ./logos/logo-yellow-slab.svg with class="project-logo". The package's logos/ directory is a synced copy of the repo-root logos/synced/ (see the notice in logos/synced/README.md), so never edit it inside a package.
  • Badges. Five badges from shields.io: npm version, npm downloads, license, and the EmberJS and WarpDrive Discord servers. Copy the block from another package of the same kind. A modern package's version and download badges name the package itself, URL-encoded (%40warp-drive%2Fcore), as @warp-drive/core does: non-Ember apps install it directly, and some modern packages version independently of the rest (@warp-drive/holodeck, warp-drive), so an ember-data version badge would be wrong for them. A legacy package keeps the ember-data targets, as @ember-data/store does: most of them ship as dependencies of the ember-data meta package and share its version, and the meta package's numbers describe the ecosystem they belong to better than their own would. An unpublished package keeps the ember-data targets too, since a badge for a name npm does not know renders as "not found". If the package wants its own release badges, add a Tagged Releases list between the H1 and the description (@warp-drive/ember has one).
  • Package name as the H1, # @warp-drive/json-api. @warp-drive/core is the exception: it is the project's front door and uses the WarpDrive tagline instead.
  • The introduction: what it is, who should use it, and why. One paragraph is enough for a small package; @warp-drive/json-api says it is a {json:api} cache implementation and that most apps should use it. A few paragraphs and a diagram are fine when they help an evaluator decide, as the two architecture diagrams in @ember-data/store do. A legacy or deprecated package opens this section with a GitHub alert (> [!WARNING]) that says so and names the replacement, as @ember-data/store does; the alert is not optional, since npm and GitHub readers never see the landing page's warning container.
  • Install and the elevator-pitch snippet. A pnpm add line and the smallest code sample that shows the package in use: a developer skimming npm decides whether to keep reading from that sample, not from prose. The landing page shows the same thing in more depth, and that overlap is intended; it is the one piece of duplication a README carries on purpose. Keep the sample to what fits on one screen and link the landing page for the rest. @warp-drive/memory-alpha is read from node_modules rather than imported, so its sample is the install and a file path.
  • ## Documentation with the same Get Started link to the Guides every package uses, followed by a link to the package's own landing page in the API docs when the docs build publishes the package (@ember-data/store has both; @ember-data/debug is not in the build and has only the first). Both are absolute URLs: the README is rendered by GitHub and npm, so the root-relative /guides/ links the rest of the docs use do not resolve here. A non-legacy package also lists the guides that teach it, mirroring the guides its own landing page in README vs src/index.md lists, with the same absolute URLs, as @warp-drive/core's README does. A legacy package's Get Started link points at Legacy Package Setup instead of the generic Guides landing page, since that page is the one a still-legacy app actually needs.
  • ## Code of Conduct and ### License, linking the repo's CODE_OF_CONDUCT.md on GitHub and the package's own LICENSE.md, which ships in files alongside the README.

Two branding blocks sit outside that order. Several READMEs open with a centered tagline <p> right under the H1. Every @warp-drive/* README closes, after the License section, with the collapsible ### ♥️ Credits block and the <style> tag inside it; copy the block from @warp-drive/vue byte for byte, since a few older copies have drifted. Legacy @ember-data/* READMEs keep whatever branding they already have. These blocks are wanted: keep them when editing a README and copy them when creating one.

A few older READMEs (@warp-drive/ember, @ember-data/request) predate this shape and carry a full manual. Do not copy that: their API detail has to be kept in sync by hand, and the link checker does not cover READMEs (see Checking Links).

README vs src/index.md ​

Each package's typedoc.config.mjs sets readme: 'src/index.md', and TypeDoc renders that file as the package's landing page in the API docs at /api/<package-name>/, above the generated member listing. It pairs with the bare @module / @mergeModuleWith <project> comment at the top of src/index.ts; Documenting Packages and Subpackages explains why both are needed. A package whose config still says readme: 'none' has a landing page that silently does not render; the fix is to set readme: 'src/index.md' and create that file, not to put the prose somewhere else.

The split follows from where each file renders:

  • README.md is GitHub-flavored markdown for GitHub and npm. Use > [!WARNING] alerts and absolute URLs. It answers "should I install this?".
  • src/index.md is VitePress markdown for the docs site. It can use :::tip and :::warning containers, ::: code-group, root-relative links like /api/@warp-drive/core/, and TSDoc {@link} references, as packages/request/src/index.md does. It starts with the package name as an H1 and answers "I am on the API docs for this package, where do I start?", so this is the place for the setup snippet, as warp-drive-packages/json-api/src/index.md shows. A {@link} here resolves against every package in the docs build, not against the imports of src/index.ts, so name the package: {@link @warp-drive/core!Store | Store}, where the ! separates the package from the symbol. For a symbol under a subpath entry point, use a root-relative URL instead, [Cache](/api/@warp-drive/core/types/cache/types/Cache); copy the path from the symbol's rendered page. A bare {@link Store} renders as plain text with a Failed to resolve link warning in the build log.

Some packages still duplicate paragraphs between the two. When you touch one, read the other and move each sentence to the file that answers its question.

End every non-legacy package's src/index.md with a ## Guides section: a list of the published guides that teach the package, each with a short phrase saying what it covers, as warp-drive-packages/json-api/src/index.md does. Guides and packages aren't one-to-one; several packages can list the same guide, and one package can list many. Don't link a draft. If no published guide teaches the package, link the nearest guide that covers what it does, and write one if nothing does.

The landing page's entry in llms.txt, the index coding agents read to decide which page to fetch, is the description in the package's package.json: the same role @summary plays for a symbol's page. Write it as one sentence saying what the package provides and when to use it. For a legacy package, start it with (Legacy) and say what replaces it; mark an internal package (Internal) and a deprecated one (Deprecated) the same way. npm shows the same text, so it serves both readers. Don't put frontmatter in src/index.md: TypeDoc renders a package readme's --- block as page text.

Keep READMEs short ​

A README links to the Guides and the API docs instead of restating them. Concepts live in a guide (see Writing Guides); signatures, options, and per-member behavior live in TSDoc, as the rest of this page describes. If a README sentence explains how something works, it belongs in one of those and the README should link there.

Short means without the manual, not without the introduction or the elevator-pitch snippet. When a landing page or guide now owns a section, remove that section from the README and link there. Everything in README structure stays: the introduction and its legacy alert, the install line and the one snippet, and every branding block. A README cut down to badges and links fails the evaluator it exists for.

When code changes, follow the Cross-Documentation Checklist: a new public API gets a README or src/index.md mention only if it changes the package's headline story, and a breaking change means fixing any README example that uses the old API. Preview README changes by viewing the file on your branch on GitHub.

Previewing and Troubleshooting ​

Previewing Documentation ​

Run pnpm start from docs-viewer/; it rebuilds the API docs when package sources change. The static build and the pull-request preview are covered in Previewing Your Changes.

Troubleshooting ​

If you have added docs but are not seeing them when previewing locally, and if you have confirmed the docs preview server is running (and has not crashed):

  • The docs may have been excluded due to using an ignored doc comment
  • The docs may have been excluded due to not using the right comment syntax
  • The documented thing may not be accessible via any public API entrypoint (the exports in the package's package.json)
  • TypeDoc may be configured to ignore it (extremely rare; see docs-viewer/typedoc.config.mjs)

Released under the MIT License.