Client-Side API for Hierarchical Resources
Learning Objectives
- You know how to implement a client-side API for hierarchical resources.
- You know how to modify the shared state to use a client-side API for hierarchical resources.
- You know how to modify pages and components to use the shared state for hierarchical resources.
In the chapter Hierarchical Resources and APIs of the part Server-Side Functionality with a Database, we created a hierarchical resource structure for books and their chapters. The structure of the API for chapters was as follows:
- GET
/api/books/{bookId}/chaptersreturns a list of chapters for a specific book. - GET
/api/books/{bookId}/chapters/{chapterId}returns a single chapter for a specific book. - POST
/api/books/{bookId}/chapterscreates a new chapter for a specific book. - PUT
/api/books/{bookId}/chapters/{chapterId}updates a chapter for a specific book. - DELETE
/api/books/{bookId}/chapters/{chapterId}deletes a chapter for a specific book.
Here, we’ll briefly walk through implementing the client-side functionality for interacting with some of the chapter-related API endpoints.
Client-side API
The client-side API for interacting with the chapters is very similar to the client-side API for interacting with the books. The key difference is that the chapters are associated with books, so — for most of the functions — we need to pass both bookId and chapterId to operate on chapters.
Create a file called chaptersApi.js in the src/lib/apis folder and add the following functionality to it. The functionality is very similar to the booksApi.js file, so we won’t go into too much detail here.
import { PUBLIC_API_URL } from "$env/static/public";
const readChapters = async (bookId) => {
const response = await fetch(
`${PUBLIC_API_URL}/api/books/${bookId}/chapters`,
);
return await response.json();
};
const readChapter = async (bookId, chapterId) => {
const response = await fetch(
`${PUBLIC_API_URL}/api/books/${bookId}/chapters/${chapterId}`,
);
return await response.json();
};
const createChapter = async (bookId, chapter) => {
const response = await fetch(
`${PUBLIC_API_URL}/api/books/${bookId}/chapters`,
{
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(chapter),
},
);
return await response.json();
};
const updateChapter = async (bookId, chapterId, chapter) => {
const response = await fetch(
`${PUBLIC_API_URL}/api/books/${bookId}/chapters/${chapterId}`,
{
headers: {
"Content-Type": "application/json",
},
method: "PUT",
body: JSON.stringify(chapter),
},
);
return await response.json();
};
const deleteChapter = async (bookId, chapterId) => {
const response = await fetch(
`${PUBLIC_API_URL}/api/books/${bookId}/chapters/${chapterId}`,
{
method: "DELETE",
},
);
return await response.json();
};
export {
createChapter,
deleteChapter,
readChapter,
readChapters,
updateChapter,
};
Shared state and API
Next, we need to adjust the shared state for chapters to use the API. When working on an exercise in the chapter Forms and Form Events, one possible structure for chapterState.svelte.js was as follows.
let chapterState = $state({});
const useChapterState = () => {
return {
get chapters() {
return chapterState;
},
addChapter: (bookId, chapter) => {
if (!chapterState[bookId]) {
chapterState[bookId] = [];
}
chapter.id = chapterState[bookId].length + 1;
chapter.book_id = bookId;
chapterState[bookId].push(chapter);
},
};
};
export { useChapterState };
Reading chapters to the state
Like with books, we need to modify the functionality so that we have a function for initializing the chapters from the API. Let’s import the chaptersApi module and the browser variable from $app/environment to check that the code is running in the browser, and add a function initBookChapters for loading the chapters of a book from the API.
import { browser } from "$app/environment";
import * as chaptersApi from "$lib/apis/chaptersApi.js";
let chapterState = $state({});
const initBookChapters = async (bookId) => {
if (!browser) {
return;
}
chapterState[bookId] = await chaptersApi.readChapters(bookId);
};
// old functionality
export { initBookChapters, useChapterState };
Now, the initBookChapters function can be used to initialize the chapters of a book from the API.
Reading all chapters at /books/[bookId]
Next, let’s modify the page that displays a single book to load the chapters when the page is loaded. In the last chapter, the file src/routes/books/[bookId]/+page.svelte looked as follows, since we omitted the chapter-specific functionality.
<script>
import Book from "$lib/components/Book.svelte";
import { initBook } from "$lib/states/bookState.svelte.js";
let { data } = $props();
let bookId = parseInt(data.bookId);
$effect(() => {
initBook(bookId);
});
</script>
<Book bookId={bookId} />
To load and show the chapters, we can import the ChapterList and ChapterForm components, which were created in an exercise in the chapter Forms and Form Events, and add them to the page.
<script>
import Book from "$lib/components/Book.svelte";
import ChapterForm from "$lib/components/books/ChapterForm.svelte";
import ChapterList from "$lib/components/books/ChapterList.svelte";
import { initBook } from "$lib/states/bookState.svelte.js";
let { data } = $props();
let bookId = parseInt(data.bookId);
$effect(() => {
initBook(bookId);
});
</script>
<Book bookId={bookId} />
<ChapterList bookId={bookId} />
<ChapterForm bookId={bookId} />
Then, to explicitly load the chapters when the page is loaded, we can import and call the initBookChapters function in the effect as well.
<script>
import Book from "$lib/components/Book.svelte";
import ChapterForm from "$lib/components/books/ChapterForm.svelte";
import ChapterList from "$lib/components/books/ChapterList.svelte";
import { initBook } from "$lib/states/bookState.svelte.js";
import { initBookChapters } from "$lib/states/chapterState.svelte.js";
let { data } = $props();
let bookId = parseInt(data.bookId);
$effect(() => {
initBook(bookId);
initBookChapters(bookId);
});
</script>
<Book bookId={bookId} />
<ChapterList bookId={bookId} />
<ChapterForm bookId={bookId} />
Now, the chapter information is loaded from the API when the page is loaded, and the chapters are displayed in the list.
Adding a chapter
We further need to modify the addChapter function to use the API. The API provides the function createChapter, which is given the bookId and the chapter to be created. The function returns the created chapter, which we can then add to the state.
Modify the addChapter function of the chapterState.svelte.js as follows.
addChapter: (bookId, chapter) => {
chaptersApi.createChapter(bookId, chapter).then((newChapter) => {
const chapters = chapterState[bookId] || [];
chapters.push(newChapter);
chapterState[bookId] = chapters;
});
},
Now, when a chapter is added, it will be created in the API and then added to the state.
The above could have also been written as an async function, as follows.
addChapter: async (bookId, chapter) => {
const newChapter = await chaptersApi.createChapter(bookId, chapter);
const chapters = chapterState[bookId] || [];
chapters.push(newChapter);
chapterState[bookId] = chapters;
},
Now, when the addChapter function is called, the chapter will be created in the API and then added to the state. With this, the functionality for adding chapters through the form also works.
Summary
In summary:
- Client-side API modules for hierarchical resources can be created similarly to non-hierarchical resources, but the functions need to take into account the hierarchy (e.g., passing both
bookIdandchapterId). - The shared state for hierarchical resources can be modified to include functions for initializing the state from the API and for adding, updating, and deleting resources using the client-side API.
- The key changes to components are similar to those for non-hierarchical resources: initializing the state when needed (e.g. at page load), and using the shared state functions to manipulate the resources.