Skip to main content

JSON:API blog CRUD

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, hasMany, makeModel } from 'foscia/core';
import type Post from './post';

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

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

Classic CRUD

View many

import { forModel, 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()
.use(forModel(Post))
.use(when(query.search, (a, s) => a.use(filterBy('search', s))))
.use(sortByDesc('createdAt'))
.use(paginate({ number: query.page ?? 1 }))
.run(all(usingDocument));
}

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

View one

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

export default async function fetchOnePost(id) {
return action().use(find(Post, id)).use(include('tags')).run(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()
.use(save(post))
.run(
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().use(destroy(post)).run(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 { forModel, forInstance, include, when, 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()
.use(forModel(Post))
.use(makeGet('actions/best-posts'))
.run(all());
}

export default function publishPost(post, query = {}) {
return action()
.use(forInstance(post))
.use(when(query.include, (a, i) => a.use(include(i))))
.use(makePost('actions/publish', {
publishedAt: new Date(),
}))
.run(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.