momentum-api
Work with Momentum API for data operations in Angular components
Best use case
momentum-api is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Work with Momentum API for data operations in Angular components
Teams using momentum-api should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/momentum-api/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How momentum-api Compares
| Feature / Agent | momentum-api | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Work with Momentum API for data operations in Angular components
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Momentum API Usage
Guide for using `injectMomentumAPI()` in Angular components.
**Key rules**: Always use async/await (never subscribe). Always use `instanceof` checks for DOM elements (never `as` type assertions). Always use typed error classes for error handling.
## Arguments
- `$ARGUMENTS` - Operation type: "query", "crud", "typed", or collection name
## Quick Reference
### Inject the API
```typescript
import { injectMomentumAPI } from '@momentumcms/admin';
@Component({...})
export class MyComponent {
private readonly api = injectMomentumAPI();
}
```
### Query Data (async/await — the only correct pattern)
Always use async/await with `.find()` and `.findById()`. Never use `.find$().subscribe()` or any Observable pattern.
```typescript
async loadData(): Promise<void> {
const result = await this.api.collection<Post>('posts').find({ limit: 10 });
this.posts.set(result.docs);
}
```
### Single Document Lookup
Use `.findById()` to fetch a single document by ID:
```typescript
async loadPost(id: string): Promise<void> {
const post = await this.api.collection<Post>('posts').findById(id);
this.post.set(post);
}
```
### CRUD Operations
```typescript
// Create
const post = await this.api.collection<Post>('posts').create({ title: 'New Post' });
// Read single document
const post = await this.api.collection<Post>('posts').findById('123');
// Update
const updated = await this.api.collection<Post>('posts').update('123', { title: 'Updated' });
// Delete
const result = await this.api.collection<Post>('posts').delete('123');
```
### With Generated Types
1. Generate types: `nx run example-angular:generate-types`
2. Import and use:
```typescript
import type { Post, User } from '../types/momentum.generated';
const posts = await this.api.collection<Post>('posts').find();
const users = await this.api.collection<User>('users').find();
```
## Find Options
Always pass `FindOptions` to `.find()` to control queries. All fields are optional:
```typescript
interface FindOptions {
where?: Record<string, unknown>; // Filter conditions (see examples below)
sort?: string; // Sort field (prefix with - for desc, e.g. '-createdAt')
limit?: number; // Max results (default: 10)
page?: number; // Page number (default: 1)
depth?: number; // Relationship population depth
transfer?: boolean; // TransferState caching (default: true)
}
```
### FindOptions Usage Examples
```typescript
// Filter by field value
const published = await this.api.collection<Post>('posts').find({
where: { status: { equals: 'published' } },
limit: 20,
sort: '-createdAt',
page: 1,
});
// Paginated query
const page2 = await this.api.collection<Post>('posts').find({
limit: 10,
page: 2,
sort: 'title',
});
// Combined filters
const filtered = await this.api.collection<Post>('posts').find({
where: { category: { equals: categoryId }, _status: { equals: 'published' } },
limit: 50,
sort: '-createdAt',
});
```
## Do / Don't
### DOM Access — never use `as` type assertions
```typescript
// DON'T — causes @typescript-eslint/consistent-type-assertions lint failure
async handleSubmit(event: Event): Promise<void> {
const form = event.target as HTMLFormElement; // LINT ERROR
const input = form.querySelector('input') as HTMLInputElement; // LINT ERROR
}
// DO — use instanceof narrowing
async handleSubmit(event: Event): Promise<void> {
event.preventDefault();
const target = event.target;
if (!(target instanceof HTMLFormElement)) return;
const input = target.querySelector('input');
if (!(input instanceof HTMLInputElement)) return;
const post = await this.api.collection<Post>('posts').create({
title: input.value,
});
this.posts.update((posts) => [post, ...posts]);
input.value = '';
}
```
### Data Fetching — always use async/await, never subscribe
```typescript
// DON'T — Observable subscribe pattern
this.api
.collection<Post>('posts')
.find$({ limit: 10 })
.subscribe((result) => {
this.posts.set(result.docs);
});
// DO — async/await pattern
const result = await this.api.collection<Post>('posts').find({ limit: 10 });
this.posts.set(result.docs);
```
### Error Handling — use error name checks (browser-safe)
> **Important:** Error classes live in `@momentumcms/server-core` (`env:server`).
> Browser components MUST NOT import from server packages. Use `error.name` checks instead.
```typescript
// DON'T — generic catch with no typed handling
try {
await this.api.collection('posts').create(data);
} catch (e) {
console.error(e);
}
// DON'T — import from @momentumcms/server-core in browser code (env boundary violation)
// import { ValidationError } from '@momentumcms/server-core'; // ❌ server-only
// DO — check error.name for browser-safe error handling
interface MomentumError extends Error {
errors?: Array<{ field: string; message: string }>;
}
try {
await this.api.collection<Post>('posts').create(data);
} catch (error) {
const err = error as MomentumError;
if (err.name === 'ValidationError' && err.errors) {
this.validationErrors.set(err.errors);
} else if (err.name === 'DocumentNotFoundError') {
this.notFound.set(true);
} else if (err.name === 'AccessDeniedError') {
this.accessDenied.set(true);
}
}
```
## Full Component Example
```typescript
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
import { injectMomentumAPI } from '@momentumcms/admin';
// Browser-safe error interface (do NOT import from @momentumcms/server-core in browser code)
interface MomentumError extends Error {
errors?: Array<{ field: string; message: string }>;
}
import type { Post } from '../types/momentum.generated';
@Component({
selector: 'app-posts',
template: `
@if (loading()) {
<p>Loading...</p>
} @else if (error()) {
<p>{{ error() }}</p>
} @else {
@for (post of posts(); track post.id) {
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.content }}</p>
<button (click)="deletePost(post.id)">Delete</button>
</article>
}
}
<form (submit)="createPost($event)">
<input #titleInput placeholder="Title" />
<button type="submit">Create Post</button>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostsComponent {
private readonly api = injectMomentumAPI();
readonly posts = signal<Post[]>([]);
readonly loading = signal(true);
readonly error = signal<string | null>(null);
constructor() {
void this.loadPosts();
}
async loadPosts(): Promise<void> {
this.loading.set(true);
try {
const result = await this.api.collection<Post>('posts').find({
limit: 20,
sort: '-createdAt',
where: { _status: { equals: 'published' } },
});
this.posts.set(result.docs);
} catch (error) {
const err = error as MomentumError;
if (err.name === 'DocumentNotFoundError') {
this.error.set('Posts collection not found.');
} else {
this.error.set('Failed to load posts.');
}
} finally {
this.loading.set(false);
}
}
async createPost(event: Event): Promise<void> {
event.preventDefault();
const target = event.target;
if (!(target instanceof HTMLFormElement)) return;
const input = target.querySelector('input');
if (!(input instanceof HTMLInputElement)) return;
try {
const post = await this.api.collection<Post>('posts').create({
title: input.value,
});
this.posts.update((posts) => [post, ...posts]);
input.value = '';
} catch (error) {
const err = error as MomentumError;
if (err.name === 'ValidationError' && err.errors) {
console.error('Validation failed:', err.errors);
}
}
}
async deletePost(id: string): Promise<void> {
await this.api.collection<Post>('posts').delete(id);
this.posts.update((posts) => posts.filter((p) => p.id !== id));
}
}
```
## Error Handling
Use `error.name` checks for browser-safe error handling (do NOT import from `@momentumcms/server-core` in browser code):
```typescript
// Browser-safe error interface
interface MomentumError extends Error {
errors?: Array<{ field: string; message: string }>;
}
try {
await this.api.collection<Post>('posts').findById(id);
} catch (error) {
const err = error as MomentumError;
if (err.name === 'DocumentNotFoundError') {
// Document with given ID does not exist
this.notFound.set(true);
} else if (err.name === 'AccessDeniedError') {
// Current user lacks permission
this.accessDenied.set(true);
} else if (err.name === 'ValidationError' && err.errors) {
// err.errors: Array<{ field: string; message: string }>
this.validationErrors.set(err.errors);
} else if (err.name === 'CollectionNotFoundError') {
// Collection slug is invalid
this.error.set('Invalid collection');
}
}
```
### Available Error Classes
| Error Class | When Thrown | Useful Properties |
| ------------------------- | ----------------------------------- | ------------------------------ |
| `ValidationError` | Create/update with invalid data | `errors: { field, message }[]` |
| `DocumentNotFoundError` | `findById` with non-existent ID | `message` |
| `AccessDeniedError` | User lacks permission for operation | `message` |
| `CollectionNotFoundError` | Invalid collection slug | `message` |
| `GlobalNotFoundError` | Invalid global slug | `message` |
## Platform Behavior
- **SSR**: Direct database access (no HTTP overhead)
- **Browser**: HTTP calls to `/api/*`
- **Same interface** - code works identically on both platforms
## Type Generation
Generate types from your collections:
```bash
# Generate types
nx run example-angular:generate-types
# Watch mode (auto-regenerate on changes)
nx run example-angular:generate-types --watch
```
Output file: `src/types/momentum.generated.ts`
## TransferState (SSR Hydration)
TransferState is **enabled by default** for all read operations (`find`, `findById`, `findSignal`, `findByIdSignal`). Data fetched during SSR is automatically cached and reused on browser hydration, eliminating duplicate HTTP calls.
### Default Behavior (TransferState enabled)
```typescript
// SSR: Fetches and caches | Browser: Reads from cache (no HTTP)
const posts = await this.api.collection<Post>('posts').find({ limit: 10 });
const post = await this.api.collection<Post>('posts').findById(id);
```
### Opt-out
Use `transfer: false` to disable TransferState for a specific call:
```typescript
// Always makes HTTP call on browser (no caching)
const posts = await this.api.collection<Post>('posts').find({
limit: 10,
transfer: false,
});
```
### Signal Methods
```typescript
// Signals also use TransferState by default
readonly posts = this.api.collection<Post>('posts').findSignal({ limit: 10 });
readonly post = this.api.collection<Post>('posts').findByIdSignal(id);
```
### Requirements
Ensure `provideClientHydration()` is in your app config:
```typescript
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration()],
};
```
<!--
## Changes from previous version and why:
1. REMOVED "Query Data (Observables)" section entirely — it showed `.find$().subscribe()` which contradicts the preferred async/await pattern and confused agents into using subscribe.
2. FIXED full component example — replaced `as HTMLFormElement` and `as HTMLInputElement` type assertions with `instanceof` narrowing to avoid @typescript-eslint/consistent-type-assertions lint failures.
3. ADDED "Do / Don't" section with three concrete anti-patterns:
- DOM access: `as` assertions vs `instanceof` narrowing
- Data fetching: subscribe vs async/await
- Error handling: generic catch vs typed error classes
4. ADDED "Single Document Lookup" section highlighting `.findById()` as the method for fetching by ID — not obvious from just seeing `.find()` examples.
5. EXPANDED FindOptions section with concrete usage examples showing `where`, `sort`, `limit`, `page` together — agents need to see these combined, not just the interface definition.
6. EXPANDED Error Handling section with a table of all error classes, when they're thrown, and their properties. Added `DocumentNotFoundError` and `AccessDeniedError` handling to the full component example.
7. ADDED `void this.loadPosts()` call pattern in constructor (matching codebase convention) instead of bare `this.loadPosts()`.
8. ADDED typed error handling to the full component example's `loadPosts()` and `createPost()` methods — previously only `finally` was shown, no catch with typed errors.
9. ADDED `where` clause to the full component example's `loadPosts()` to demonstrate filtering, which agents wouldn't learn from just seeing the interface.
10. Preserved all passing sections: injection, CRUD, generated types, platform behavior, type generation, TransferState.
-->Related Skills
headless-ui
Use @momentumcms/headless inside generated Momentum apps. Use when building custom public UI, composing accessible primitives, configuring global styles for hdl-* elements, or adding app-level tests around headless interactions.
ui-audit
Comprehensive UI component audit for Momentum CMS. Use when asked to audit, review, check, or validate a UI component. Checks Storybook stories, interaction tests, variants, kitchen sink integration, admin dashboard usage, accessibility, and responsive design (mobile-first). AUTOMATICALLY FIXES issues found and verifies with visual inspection. Triggers include "audit button", "review the card component", "check accessibility of tabs", or "/ui-audit <component-name>".
test-all
Run the FULL Momentum CMS test suite — every single suite, no skips. Triggers on "test all", "test everything", "run all tests", "run the test all script", "test-all script", "run the full suite", "run every test", "test the whole thing", "make sure everything passes", "run test:all", or ANY variation asking to run all/every/full tests. Also triggers on typos like "test al", "tets all", "tes all". NEVER skip suites unless the user EXPLICITLY names suites to skip.
stroll-test
End-to-end CLI stroll test of npm-published Momentum CMS packages. Scaffolds a fresh project with create-momentum-app, adds all plugins, runs migrations, starts server, and verifies everything works. Triggers include "stroll test", "cli stroll", "test published packages", or "/stroll-test".
skill-improve
Self-improving skill loop. Analyzes eval failures, rewrites the skill, re-evaluates, and repeats until convergence. Run after /skill-eval produces baseline results.
skill-eval
Run structured evaluations comparing skill vs no-skill performance. Measures assertion pass rates, timing, and output quality to systematically improve skills.
prepare-release
Prepare a patch/minor/major version release for all Momentum CMS packages. Bumps versions, generates changelogs, verifies builds/tests, adds new packages to Nx release config, and commits. Triggers include "prepare release", "bump version", "release patch", or "/prepare-release".
migrations
Run migrations, generate schemas, and manage code generation for Momentum CMS. Use when working with database migrations, Drizzle schema generation, type generation, or Angular schematics.
mcp-setup
Set up the Momentum CMS MCP server plugin and generate Claude Code MCP config for AI tool integration. Use when connecting Claude Code (or any MCP client) to a Momentum CMS instance.
SYSTEM ROLE & BEHAVIORAL PROTOCOLS
**ROLE:** Senior Frontend Architect & Avant-Garde UI Designer.
headless-primitive
Author, extend, or repair primitives in libs/headless. Use when adding a new headless primitive, changing its accessibility contract, updating slots/state attrs, wiring overlay behavior, or expanding the example styling lab and tests.
e2e-test
Write and validate Playwright E2E tests for Momentum CMS features. UI tests ALWAYS start from /admin dashboard and navigate via sidebar/dashboard — never go directly to deep URLs. Always starts the server and inspects the actual UI before writing assertions. Triggers include "write e2e tests for...", "add e2e tests", "test the admin UI for...", or "/e2e-test <feature>".