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,
};

Forgetting bookId in chapter API

0 / 5 points

Using the chaptersApi.js, a developer writes an await readChapter(chapterId) function without including bookId. What will happen when this function is used?

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 { page } from "$app/state";
import Book from "$lib/components/Book.svelte";
import { initBook } from "$lib/states/bookState.svelte.js";
let bookId = $derived(parseInt(page.params.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 { page } from "$app/state";
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 bookId = $derived(parseInt(page.params.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 { page } from "$app/state";
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 bookId = $derived(parseInt(page.params.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.

Initializing chapters

0 / 5 points

A team notices that chapters are not showing when loading /books/[bookId]. The page imports ChapterList, but initBookChapters(bookId) was not called. Why are the chapters missing?

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.

Alternative implementation

0 / 5 points

A developer implements addChapter like this:

add: (bookId, chapter) => {
const newChapter = chaptersApi.createChapter(bookId, chapter);
const chapters = chaptersByBookState[bookId] || [];
chapters.push(newChapter);
chapterState[bookId] = chapters;
}

What problem will occur?

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.

Todos and tasks, tasks with an API

0 / 50 points

Continue working from your solution to the earlier “Todos and tasks, todos with an API” exercise (from the end of the last chapter). At the end of the exercise, the todos were backed by an API, but the tasks were still stored in the shared state and localstorage.

In this exercise, you’ll modify the application so that the tasks are also stored in the database and accessed through the API.

Api module

First, implement an API module for tasks. For this, create a new file called tasksApi.js in the src/lib/apis folder. The module should export the functions needed to interact with the tasks API you implemented in the “Todo Controller and Repository” exercise. The API endpoints at focus are as follows:

  • POST /api/todos/:todoId/tasks -> create a new task for a given todo and return the created task
  • GET /api/todos/:todoId/tasks -> read and return all tasks for a given todo
  • GET /api/todos/:todoId/tasks/:taskId -> read and return a single task by its id
  • PUT /api/todos/:todoId/tasks/:taskId -> update and return a task by its id
  • DELETE /api/todos/:todoId/tasks/:taskId -> delete and return a task by its id

The data format for a task is as follows.

{
"id": 1,
"todo_id": 1,
"description": "Set up project structure",
"is_done": false,
"created_at": "2025-01-01T12:45:00.000Z"
}

Modifying the shared state

Then, modify the shared state taskState.svelte.js for tasks so that it uses the API module to interact with the tasks. Create also any necessary functions for initiating the tasks for a given todo from the API.

Verifying and modifying the pages

Finally, modify and verify the pages for showing an individual todo and showing an individual task. Their functionality should be as follows.

  • /todos/[todoId] — shows the details of a single todo, including tasks.

    • The individual todo and the tasks for the individual todo should be fetched from the API when the page is loaded.
    • The page should show the todo and a list of tasks for the todo, as well as a form for adding a new task.
    • Adding a task should create the task through the API and then update the shared state accordingly.
    • Toggling a task as done/undone should update the task through the API and then update the shared state accordingly.
    • Deleting a task should delete the task through the API and then update the shared state accordingly.
  • /todos/[todoId]/tasks/[taskId] — shows the details of a single task.

    • The individual task should be fetched from the API when the page is loaded.

Important! Do not change the phrasing of the text in the components from your earlier version of the application. The tests expect the same formatting and text as before — now, the tasks are just fetched from an API instead of being created and stored locally.

Submission format

Once ready, zip the contents of the folder client/src and return the zip here. The zip should not contain the client/src folder, but the contents of the client/src folder. The contents of the zip should be as follows.

Terminal window
.
├── lib
├── apis
├── tasksApi.js
└── todosApi.js
├── components
└── todos
├── Task.svelte
├── TaskForm.svelte
├── TaskList.svelte
├── Todo.svelte
├── TodoForm.svelte
└── TodoList.svelte
└── states
| ├── taskState.svelte.js
| └── todoState.svelte.js
└── routes
├── todos
├── [todoId]
├── tasks
└── [taskId]
└── +page.svelte
└── +page.svelte
└── +page.svelte
└── +page.svelte