Forms and Client-Server Interaction

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}/chapters returns 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}/chapters creates 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,
};
Loading Exercise...

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.

Loading Exercise...

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.

Loading Exercise...

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 bookId and chapterId).
  • 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.
Loading Exercise...