Skip to content

API Server ​

Getting Started serves one storage repository over HTTP with buildJsonApi. When an app needs documents, storage, and assets behind one endpoint, with authentication built in, use laikaApi.

bash
pnpm add laikacms @laikacms/server
typescript
import { laikaApi } from '@laikacms/server/api';
import { CatalogAssetsRepository } from 'laikacms/assets/catalog';
import { ConventionCatalogProvider } from 'laikacms/catalog-convention';
import { CatalogDocumentsRepository } from 'laikacms/documents/catalog';
import { jsonSerializer } from 'laikacms/serializers/json';
import { FileSystemStorageRepository } from 'laikacms/storage/fs';

const storage = new FileSystemStorageRepository('./content', { json: jsonSerializer }, 'json');
const catalog = new ConventionCatalogProvider({ storage });
const documents = new CatalogDocumentsRepository(storage, catalog);
const assets = new CatalogAssetsRepository(storage, catalog);

const api = laikaApi({
  documents,
  storage,
  assets,
  basePath: '/api',
  // WHO the caller is. Throw to reject the token.
  authenticateAccessToken: async token => {
    const session = await db.sessions.findByAccessToken(token);
    if (!session) throw new Error('Invalid session');
    return db.users.findById(session.userId);
  },
  // WHAT they may do. Return false, or throw, to deny.
  authorize: ctx => ctx.operation === 'read',
});

export default { fetch: api.fetch };

Like buildJsonApi, laikaApi returns a standard fetch(request): Promise<Response> handler, so it runs on Node.js, Bun, Deno, Cloudflare Workers, or anything else that speaks the Fetch API.

Options ​

OptionTypeRequiredDescription
documentsDocumentsRepositoryyesDocument storage backend
storageStorageRepositoryyesRaw file storage backend
assetsAssetsRepositorynoBinary asset storage; enables the /assets endpoint when provided
basePathstringnoURL prefix for all endpoints (e.g. '/api')
authenticateAccessToken(token: string) => Promise<User>yesValidates a Bearer access token and returns the principal's identity
authenticateApiToken(key: string) => Promise<User>noValidates an API key sent via X-API-Key or Authorization: ApiKey for M2M access
authorize(ctx: AuthorizeContext) => boolean | Promise<boolean>yesThe authorization gate; return false to reject with 403. Fails closed if it throws.
loggerPick<Console, 'error'|'warn'|'info'|'debug'>noReceives structured diagnostic output
corsCorsOptionsnoRequired when the client is served from a different origin than the API

authorize: the authorization gate ​

Authentication answers who the caller is; authorize(ctx) answers what they may do. There is no implicit default: you must state the policy, and a policy that throws counts as a denial.

ts
// Allow everything authenticated:
authorize: () => true,

// Read-only:
authorize: ctx => ctx.operation === 'read',

// Role-based:
authorize: ctx =>
  ctx.operation === 'read' ? true : ctx.user.roles.includes('editor'),

ctx carries the user, the domain (documents, storage, assets, …), the operation (read, create, update, delete, publish, unpublish), the collection and itemId when present, the upper-cased HTTP method, and the raw request.

See Authentication for API keys, scope-based policies, personal access tokens, and a complete OAuth2 login server.

Released under the MIT License.