Skip to main content

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:

  1. Navigate to your organization's Composable Content section

  2. Click "New Model"

  3. Define your content structure:

    • Name: Employee (this becomes ansatt in our example)
    • Fields:
      • navn (Text) - Employee name
      • avdeling (Text) - Department
      • email (Email) - Contact email
      • telefon (Text) - Phone number
      • bilde (Asset) - Profile image
      • sortering (Number) - Sort order
  4. Add data in the Content Management Tab

  5. 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

  1. Use TypeScript: Take advantage of the generated types for better developer experience
  2. Implement Caching: Consider using libraries like React Query or SWR for client-side caching
  3. Handle Loading States: Always show loading indicators while fetching data
  4. Error Boundaries: Implement proper error handling in your UI components
  5. Optimize Queries: Only fetch the fields you need and use appropriate pagination limits