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
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.
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:
async loadPost(id: string): Promise<void> {
const post = await this.api.collection<Post>('posts').findById(id);
this.post.set(post);
}
CRUD Operations
// 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
- Generate types:
nx run example-angular:generate-types - Import and use:
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:
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
// 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
// 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
// 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. 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);
}
}
Full Component Example
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):
// 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:
# 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)
// 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:
// Always makes HTTP call on browser (no caching)
const posts = await this.api.collection<Post>('posts').find({
limit: 10,
transfer: false,
});
Signal Methods
// 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:
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideClientHydration()],
};
Converted and distributed by TomeVault — claim your Tome and manage your conversions.