Skip to main content

JSON:API blog

What you'll learn
  • Defining blog and tag models
  • Use blog model for CRUD operations on a JSON:API backend

This is a simple example to implement a blog content management using Foscia. This example is framework-agnostic, so you'll only see examples of models or actions calls. You may use those examples inside any project (Vanilla JS, React, Vue, etc.).

Models

models/tag.ts
import { attr, makeModel } from '@foscia/core';

export default class Tag extends makeModel('tags', {
name: attr<string>(),
}) {}
models/post.ts
import { attr, hasMany, makeModel, toDateTime } from '@foscia/core';
import Tag from './tag';

export default class Post extends makeModel('posts', {
title: attr<string>(),
description: attr<string>(),
publishedAt: attr(toDateTime()).nullable(),
tags: hasMany(() => Tag),
get published() {
return !!this.publishedAt;
},
}) {}

Action

import { makeActionFactory, makeCache } from '@foscia/core';
import {
makeJsonApiAdapter,
makeJsonApiDeserializer,
makeJsonApiSerializer,
} from '@foscia/jsonapi';

export default makeActionFactory({
...makeCache(),
...makeJsonApiDeserializer(),
...makeJsonApiSerializer(),
...makeJsonApiAdapter({
baseURL: '/api/v1',
}),
});

Classic CRUD

View many

import { query, when, all } from '@foscia/core';
import { filterBy, sortByDesc, paginate, usingDocument } from '@foscia/jsonapi';
import action from './action';
import Post from './models/post';

export default async function fetchAllPost(query = {}) {
return action().run(
query(Post),
when(query.search, (a, s) => a.use(filterBy('search', s))),
sortByDesc('createdAt'),
paginate({ number: query.page ?? 1 }),
all(usingDocument),
);
}

const { instances, document } = await fetchAllPost({ search: 'Hello' });

View one

import { query, include, oneOrFail } from '@foscia/core';
import action from './action';
import Post from './models/post';

export default async function fetchOnePost(id) {
return action().run(query(Post, id), include('tags'), oneOrFail());
}

const post = await fetchOnePost('123-abc');

Create or update one

import { changed, fill, oneOrCurrent, reset, save, when } from '@foscia/core';
import action from './action';
import Post from './models/post';

export default async function savePost(post, values = {}) {
fill(post, values);

try {
await action().run(
save(post),
when(!post.$exists || changed(post), oneOrCurrent(), () => instance),
);
} catch (error) {
reset(post);

throw error;
}

return post;
}

const post = new Post();

await savePost(post, {
title: 'Hello World!',
publishedAt: new Date(),
});

Delete one

import { destroy, none } from '@foscia/core';
import action from './action';
import Post from './models/post';

export default async function deletePost(post) {
await action().run(destroy(post), none());
}

const post = new Post();

await deletePost(post);

Non-standard actions

You can also use Foscia to run non-standard actions to your backend.

Thanks to functional programming, you can easily combine non-standard action with classical context enhancers and runners.

import { query, include, when, all, oneOrFail } from '@foscia/core';
import { makeGet, makePost } from '@foscia/http';
import action from './action';
import Post from './models/post';

export default function bestPosts() {
return action().run(
query(Post),
.makeGet('actions/best-posts'),
all(),
);
}

export default function publishPost(post, query = {}) {
return action().run(
query(post),
when(query.include, (a, i) => a.use(include(i))),
makePost('actions/publish', {
publishedAt: new Date(),
}),
oneOrFail(),
);
}

// Sends a GET to "<your-base-url>/posts/actions/best-posts
// and deserialize a list of Post instances.
const posts = await bestPosts();

const post = new Post();
// Sends a POST to "<your-base-url>/posts/<id>/actions/publish
// and deserialize a Post instance.
await publishPost(post);
info

makeGet or other custom request enhancers (makePost, etc.) will just append the given path if it is not an "absolute" (starting with a scheme such as https://) path. This allows you to run non-standard actions scoped to an instance, etc.

Your may also use an absolute (starting with a scheme) path like https://example.com/some/magic/action to ignore the configured base URL and run a non-standard action.