with one click
momentum-api
Work with Momentum API for data operations in Angular components
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Work with Momentum API for data operations in Angular components
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Adversarial review checklist for stateful Momentum CMS features (workflow, versions, scheduled publish, permissions inheritance, branches, anything that adds writeable state + access control). Use BEFORE merging a feature that introduces new mutating routes, new system-managed columns, new access functions, or new audit trails. Trigger phrases include "red-team this", "adversarial review", "security review of <feature>", "before merging <feature>", "/cms-feature-red-team".
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.
Generate a new Momentum CMS collection with fields, access control, and hooks
Generate an Angular component with signals, OnPush, and host-based styling following Momentum CMS conventions. Use when creating new UI components in any library.
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>".
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.
| name | momentum-api |
| description | Work with Momentum API for data operations in Angular components |
| argument-hint | <operation> [collection] |
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 - Operation type: "query", "crud", "typed", or collection nameimport { injectMomentumAPI } from '@momentumcms/admin';
@Component({...})
export class MyComponent {
private readonly api = injectMomentumAPI();
}
Always use async/await with .find() and .findById(). Never use .find$().subscribe() or any Observable pattern.
async loadData(): Promise<void> {
const result = await this.api.collection<Post>('posts').find({ limit: 10 });
this.posts.set(result.docs);
}
Use .findById() to fetch a single document by ID:
async loadPost(id: string): Promise<void> {
const post = await this.api.collection<Post>('posts').findById(id);
this.post.set(post);
}
// 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');
nx run example-angular:generate-typesimport 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();
Always pass FindOptions to .find() to control queries. All fields are optional:
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)
}
// 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',
});
as type assertions// 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 = '';
}
// 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);
Important: Error classes live in
@momentumcms/server-core(env:server). Browser components MUST NOT import from server packages. Useerror.namechecks instead.
// 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);
}
}
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));
}
}
Use error.name checks for browser-safe error handling (do NOT import from @momentumcms/server-core in browser code):
// 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');
}
}
| 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 |
/api/*Generate types from your collections:
# 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 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.
// 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);
Use transfer: false to disable TransferState for a specific call:
// Always makes HTTP call on browser (no caching)
const posts = await this.api.collection<Post>('posts').find({
limit: 10,
transfer: false,
});
// 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);
Ensure provideClientHydration() is in your app config:
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration()],
};