Getting Started with CMS API
This guide will walk you through setting up and using the Streams Platform CMS API to manage custom content types in your applications.
Prerequisites
- Node.js 16+ and npm/yarn
- TypeScript knowledge (recommended)
- Basic understanding of GraphQL concepts
- A Streams Platform organization account
Step 1: Create Your Content Types
First, create your custom content types in the Streams Platform dashboard. For this example, we'll create an "Employee" content type:
-
Navigate to your organization's Composable Content section
-
Click "New Model"
-
Define your content structure:
- Name:
Employee
(this becomesansatt
in our example) - Fields:
navn
(Text) - Employee nameavdeling
(Text) - Departmentemail
(Email) - Contact emailtelefon
(Text) - Phone numberbilde
(Asset) - Profile imagesortering
(Number) - Sort order
- Name:
-
Add data in the Content Management Tab
-
Go to Web Studio and create a website (if you haven't already). From the main folder hit the three dots and click 'Create Composable Content API Client'
Step 2: A Graphql Client is generated
A service to handle GraphQL queries to the CMS API:
// query-cms-gql.service.ts
import type { GqlResponse } from './types';
export const queryCmsGql = async <T>(
query: string,
variables?: object
): Promise<T> => {
const response = await fetch('https://cms.streamscloud.com/graphql', {
method: 'POST',
headers: {
'ClientId': 'your-organization-id', // Replace with your organization ID
'Content-Type': 'application/json'
},
body: JSON.stringify({
query,
variables
})
});
const gql: GqlResponse<T> = await response.json();
if (!gql.data) {
throw new Error(JSON.stringify(gql.errors));
}
return gql.data;
};
Step 3: Generated Type Definitions
Based on your content type, we will automatically generate TypeScript interfaces and API clients. Here's an example for the Employee content type:
// employee-cms-api-client.ts
import { queryCmsGql } from './query-cms-gql.service';
import { type Asset, AssetTypes } from './types';
export type Employee = {
id: string;
createdAt: Date;
lastModifiedAt: Date;
navn: string | null;
avdeling: string | null;
email: string | null;
telefon: string | null;
bilde: Asset | null;
sortering: number | null;
};
export const EmployeeApi = {
// Get with cursor-based pagination
get: async (
cursor?: EmployeeCursorInput,
filter?: EmployeeFilterInput
): Promise<EmployeeCursorResult> => {
const data = await queryCmsGql<EmployeeCursorData>(
`query Employee($cursor: employeeCursorInput, $filter: employeeFilterInput) {
employee(cursor: $cursor, filter: $filter) {
items {
id
createdAt
lastModifiedAt
navn
avdeling
email
telefon
bilde {
blobId
contentType
name
size
thumbnailBlobId
thumbnailUrl
type
url
}
sortering
}
continuationToken
}
}`,
{ cursor, filter }
);
return {
...data.employee,
items: data.employee.items.map(convertDateFields)
};
},
// Get with page-based pagination
getPage: async (
pagination?: EmployeePaginationInput,
filter?: EmployeeFilterInput
): Promise<EmployeePageResult> => {
const data = await queryCmsGql<EmployeePageData>(
`query EmployeePage($pagination: employeePaginationInput, $filter: employeeFilterInput) {
employeePage(pagination: $pagination, filter: $filter) {
items {
id
createdAt
lastModifiedAt
navn
avdeling
email
telefon
bilde {
blobId
contentType
name
size
thumbnailBlobId
thumbnailUrl
type
url
}
sortering
}
total
}
}`,
{ pagination, filter }
);
return {
...data.employeePage,
items: data.employeePage.items.map(convertDateFields)
};
}
};
// Type definitions for pagination and filtering
export type EmployeeCursorResult = {
items: Employee[];
continuationToken?: string;
};
export type EmployeePageResult = {
items: Employee[];
total?: number;
};
export type EmployeeFilterInput = {
ids?: string[];
excludeIds?: string[];
navn?: {
eq?: string;
contains?: string;
};
avdeling?: {
eq?: string;
contains?: string;
};
email?: {
eq?: string;
contains?: string;
};
sortering?: {
eq?: number;
min?: number;
max?: number;
};
};
export enum EmployeeOrderBy {
CreatedAt = 'CREATED_AT',
LastModifiedAt = 'LAST_MODIFIED_AT',
Navn = 'NAVN',
Avdeling = 'AVDELING',
Email = 'EMAIL',
Sortering = 'SORTERING',
}
export type EmployeeCursorInput = {
continuationToken?: string;
limit?: number;
ascendingOrder?: boolean;
orderBy?: EmployeeOrderBy;
};
export type EmployeePaginationInput = {
ascendingOrder?: boolean;
orderBy?: EmployeeOrderBy;
fetchTotal?: boolean;
offset?: number;
page?: number;
perPage?: number;
};
// Internal types for GraphQL responses
type EmployeeCursorData = {
employee: EmployeeCursorResult;
};
type EmployeePageData = {
employeePage: EmployeePageResult;
};
// Helper function to convert date strings to Date objects
const convertDateFields = (item: Employee): Employee => {
return {
...item,
createdAt: new Date(item.createdAt),
lastModifiedAt: new Date(item.lastModifiedAt),
};
};
Step 4: Use in Your Application
Now you can use your generated API client in your application. Here's an example using SvelteKit:
// src/routes/employees/+page.ts
import { EmployeeApi, EmployeeOrderBy } from '$lib/cms-api-client';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
// Fetch all employees, sorted by the sorting field
const employees = (await EmployeeApi.getPage({
perPage: 1000,
orderBy: EmployeeOrderBy.Sortering,
ascendingOrder: true
})).items;
// Filter employees by department
const adminEmployees = (await EmployeeApi.getPage({
perPage: 100,
orderBy: EmployeeOrderBy.Sortering
}, {
avdeling: {
eq: 'Administration'
}
})).items;
return {
employees,
adminEmployees
};
};
<!-- src/routes/employees/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
let { employees, adminEmployees } = data;
</script>
<div class="employees">
<h1>All Employees</h1>
{#each employees as employee}
<div class="employee-card">
{#if employee.bilde}
<img src={employee.bilde.thumbnailUrl} alt={employee.navn} />
{/if}
<h3>{employee.navn}</h3>
<p>{employee.avdeling}</p>
<a href="mailto:{employee.email}">{employee.email}</a>
</div>
{/each}
</div>
Step 5: Advanced Features
Filtering and Searching
// Search employees by name
const searchResults = await EmployeeApi.getPage({
perPage: 20
}, {
navn: {
contains: 'John'
},
avdeling: {
eq: 'Engineering'
}
});
// Get specific employees by IDs
const specificEmployees = await EmployeeApi.getPage({
perPage: 10
}, {
ids: ['employee-1', 'employee-2', 'employee-3']
});
Pagination
// Cursor-based pagination (recommended for large datasets)
let continuationToken: string | undefined;
const allEmployees = [];
do {
const result = await EmployeeApi.get({
continuationToken,
limit: 50,
orderBy: EmployeeOrderBy.CreatedAt
});
allEmployees.push(...result.items);
continuationToken = result.continuationToken;
} while (continuationToken);
// Page-based pagination
const page1 = await EmployeeApi.getPage({
page: 1,
perPage: 20,
fetchTotal: true
});
console.log(`Showing ${page1.items.length} of ${page1.total} employees`);
Error Handling
try {
const employees = await EmployeeApi.getPage({
perPage: 100
});
// Handle success
console.log('Loaded employees:', employees.items);
} catch (error) {
// Handle GraphQL or network errors
console.error('Failed to load employees:', error);
// You can parse the error to get more specific information
if (error instanceof Error) {
const errorData = JSON.parse(error.message);
console.error('GraphQL errors:', errorData);
}
}
Best Practices
- Use TypeScript: Take advantage of the generated types for better developer experience
- Implement Caching: Consider using libraries like React Query or SWR for client-side caching
- Handle Loading States: Always show loading indicators while fetching data
- Error Boundaries: Implement proper error handling in your UI components
- Optimize Queries: Only fetch the fields you need and use appropriate pagination limits