Forms and Client-Server Interaction

Client-Side API Modules


Learning Objectives

  • You know common conventions for creating client-side API modules.
  • You know how to create a client-side API module for CRUD operations.

In this part so far, we’ve worked with third-party APIs to learn the basics of the Fetch API and to understand Same-Origin Policy and CORS. We’ve also looked into how to run code when a component is mounted.

Now, we’ll start working on functionality that interacts with our own server-side APIs. In this chapter, we’ll learn how to create a client-side API module for CRUD (Create, Read, Update, Delete) operations on server-side APIs that we created in the part on Server-Side Functionality with a Database.

Nothing magical here — the term module in JavaScript refers to a file that contains code, which can include variables, functions, classes, etc. You can export variables, functions, or classes from a module using the export keyword, and you can import them into other modules using the import keyword.

Common Conventions

There are two common conventions for creating client-side API functionality. First, we want to define the API URL in an environment variable, and second, we want to create a folder to store the API modules.

Environment variable

When using Svelte, we want to define the root address of the API in an environment file, which we then import from $env/static/public. Create a file called .env.development in the root of the client-side project, and define the server-side API URL there. If you have not changed the port on which the server is running, the URL would be http://localhost:8000.

That is, the contents of .env.development should be:

PUBLIC_API_URL=http://localhost:8000

After restarting the project with docker compose down and docker compose up --build, the environment variable can be accessed in the client-side code as follows.

import { PUBLIC_API_URL } from "$env/static/public";

Note that the variable name must start with PUBLIC_ to be accessible from the client-side code.

Environment variable for API URL

0 / 5 points

A developer defines an API URL in .env.development as:

API_URL="http://localhost:8000"

And try to use the variable as follows in a Svelte component:

import { API_URL } from "$env/static/public";

The application crashes. Why does this happen?

Creating a folder for API modules

A good practice is to create a folder called apis in src/lib of the client-side project. This folder will contain the API modules that we create. Each module can then be imported into the components that need to use it. This way, the code is more organized and easier to maintain.

Books API Module

Let’s start by building a client-side API module for CRUD (Create, Read, Update, Delete) operations on a server API that manages books. As a reminder, the server-side API for the books is as follows:

  • GET /api/books returns a list of books.
  • GET /api/books/{id} returns a single book.
  • POST /api/books creates a new book.
  • PUT /api/books/{id} updates a book.
  • DELETE /api/books/{id} deletes a book.

To get started, create a folder called apis in src/lib of the client-side project, if you did not already do so. Place an empty file called booksApi.js in src/lib/apis. This file will contain the client-side API functionality that we will create.

Finally, if you did not modify the .env.development file yet, do so now. The contents of .env.development should be:

PUBLIC_API_URL=http://localhost:8000

Reading all books

To start, let’s implement the functionality for reading all the books from the server-side API. We will create a function that makes a GET request to the /api/books endpoint and returns the list of books in JSON format. This would look as follows.

import { PUBLIC_API_URL } from "$env/static/public";
const readBooks = async () => {
const response = await fetch(`${PUBLIC_API_URL}/api/books`);
return await response.json();
};
export { readBooks };

Reading a single book

Reading a single book is similar to reading all books, but we need to specify the ID of the book we want to read. The id would be passed as a parameter to the function, and the function would make a GET request to the /api/books/{id} endpoint. The function would look as follows.

// ...
const readBook = async (id) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`);
return await response.json();
};
export { readBook, readBooks };

Creating a new book

To create a new book, we need to make a POST request to the /api/books endpoint. The request body should contain the details of the book we want to create, such as its title, which we can pass as a book object. The function would receive the object as a parameter and return the created book in JSON format. The function would look as follows.

// ...
const createBook = async (book) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books`, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(book),
});
return await response.json();
};
export { createBook, readBook, readBooks };

Updating a book

Updating a book is similar to creating a new book, but we need to specify the ID of the book we want to update. The function would make a PUT request to the /api/books/{id} endpoint, passing the updated book details in the request body. The function would look as follows.

// ...
const updateBook = async (id, book) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`, {
headers: {
"Content-Type": "application/json",
},
method: "PUT",
body: JSON.stringify(book),
});
return await response.json();
};
export { createBook, readBook, readBooks, updateBook };

Updating a book

0 / 5 points

A developer wants to update a book with the ID 5. Their code for the fetch looks as follows:

fetch(`${PUBLIC_API_URL}/api/books`, {
method: "PUT",
body: JSON.stringify(book)
});

What problem will this cause?

Deleting a book

Finally, to delete a book, we need to make a DELETE request to the /api/books/{id} endpoint. The function would receive the ID of the book to be deleted as a parameter and return the deleted book in JSON format. The function would look as follows.

// ...
const deleteBook = async (id) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`, {
method: "DELETE",
});
return await response.json();
};
export { createBook, deleteBook, readBook, readBooks, updateBook };

Books API together

Together, the functionality for accessing the server-side API would look as follows.

import { PUBLIC_API_URL } from "$env/static/public";
const readBooks = async () => {
const response = await fetch(`${PUBLIC_API_URL}/api/books`);
return await response.json();
};
const readBook = async (id) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`);
return await response.json();
};
const createBook = async (book) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books`, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: JSON.stringify(book),
});
return await response.json();
};
const updateBook = async (id, book) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`, {
headers: {
"Content-Type": "application/json",
},
method: "PUT",
body: JSON.stringify(book),
});
return await response.json();
};
const deleteBook = async (id) => {
const response = await fetch(`${PUBLIC_API_URL}/api/books/${id}`, {
method: "DELETE",
});
return await response.json();
};
export { createBook, deleteBook, readBook, readBooks, updateBook };
CRUD Pattern

Notice how the module follows the CRUD pattern: it contains functions for creating, reading, updating, and deleting books. This makes the code easier to understand and maintain, as each function has a clear purpose.


Client-side plants API

0 / 20 points

Your friend tells you about an idea for creating an application for managing plants. Your friend is quite apt at building server-side functionality, but has little experience in building client-side functionality. The server-side API that your friend describes is as follows:

  • GET /api/plants — returns a JSON array of plants.
  • POST /api/plants — creates a plant. Body: { name, price, inStock }. Responds with the created plant.
  • PUT /api/plants/{id} — updates a plant. Body: { name, price, inStock }. Responds with the updated plant.
  • DELETE /api/plants/{id} — deletes a plant. Responds with the deleted plant.

Each plant has the following structure:

{
"id": 1,
"name": "Monstera",
"price": 25,
"inStock": true
}

You’ve already drafted the user interface into $lib/components/Plants.svelte. Complete the client-side API module $lib/apis/plantsApi.js so that it works with the server-side API described above and so that it can be used by the existing Plants.svelte (without any modifications to it).

Using the Books API

To use the booksApi.js module we just created, we can import it into our Svelte components. For example, if we would want to read and list all the book titles when a user presses a button, we could have a component similar to the following.

<script>
import { readBooks } from "$lib/apis/booksApi.js";
let books = $state([]);
const loadBooks = async () => {
books = await readBooks();
};
</script>
<h1>Books</h1>
<button onclick={loadBooks}>Load Books</button>
<ul>
{#each books as book}
<li>{book.title}</li>
{/each}
</ul>

This component imports the readBooks function from the booksApi.js module and uses it to fetch the list of books when the button is clicked. The fetched books are stored in a state variable called books, which is then used to render a list of book titles.

For this to work, the server-side API must be running, and the client-side application must be able to access it via the URL specified in the .env.development file. Furthermore, the server-side API should have the necessary CORS headers set to allow requests from the client-side application.

Figure 1 below shows a sequence diagram that highlights what happens on the client-side when the user clicks the “Load Books” button.

Fig 1. — When the user clicks the “Load Books” button, the component calls the readBooks function of booksApi.js, which fetches the list of books from the server-side API. Then, once the books are retrieved, the component renders the books, which are then shown to the user.

For completeness, Figure 2 below shows a sequence diagram that outlines the sequence of events on the server, starting from the GET /api/books request from the client-side API, and ending with the JSON response containing the list of books from the database.

Fig 2. — The server-side API receives the GET /api/books request, which is mapped to the readAll function of the controller. The controller then calls the readAll method of the repository, which in turn makes a query to the database. The repository retrieves all books from the database and returns them to the controller, which then sends the books back to the client in JSON format.

Using booksApi

0 / 5 points

A developer tries to render a list of books with the following Svelte component:

<script>
import { readBooks } from "$lib/apis/booksApi.js";
let books = $state([]);
const loadBooks = () => {
books = readBooks();
};
</script>
<button onclick={loadBooks}>Load Books</button>
<ul>
{#each books as book}
<li>{book.title}</li>
{/each}
</ul>

What will happen when the button “Load Books” is clicked?

Similarly, we could adjust the component to use the $effect rune, which would lead to the books being loaded when the component is mounted. This would look as follows.

<script>
import { readBooks } from "$lib/apis/booksApi.js";
let books = $state([]);
const loadBooks = async () => {
books = await readBooks();
};
$effect(() => {
loadBooks();
});
</script>
<h1>Books</h1>
<button onclick={loadBooks}>Load Books</button>
<ul>
{#each books as book}
<li>{book.title}</li>
{/each}
</ul>

Plant form

0 / 15 points

The assignment comes with a solution to the previous assignment, where you modified the client-side API functionality for the plant application. Now, modify the Plants.svelte so that it has a form for adding a plant. The form should have the following input fields with labels:

  • The name of the plant (text input), label “Name”
  • The price of the plant (number input), label “Price”
  • A checkbox for whether the plant is inStock, label “In stock”
  • A submit button with the text “Add plant”

Further, modify the addPlant function so that it handles the form submission, making an API call to add the plant. Add the response from the API to the list of plants.

Summary

In summary:

  • When working with server-side APIs, it is a good practice to define the API URL in an environment variable.
  • Client-side API modules should be stored in a dedicated folder, such as src/lib/apis.
  • Client-side API modules often reflect the operations on the server-side API; if the server-side API follows the CRUD pattern, the client-side API module often does as well.

Liking resources

0 / 40 points

Prior to starting this exercise, briefly revisit the chapter “Routing and Dynamic Pages” in “Client-Side Pages, Components, and Interactivity”. Here, you’ll rehearse the concepts of routing and dynamic pages in Svelte, connecting those to client-side APIs.

In this assignment, your task is to build an application that allows liking various resources. Each resource is identified by a unique resourceId, and the like count for each resource is managed through a simple API.

The API is already implemented for you, although you do not have access to it. Your task is to create the client-side functionality to interact with this API. The API specification is as follows:

  • GET /api/resources/{resourceId}/likes —> { "likes": number }
  • POST /api/resources/{resourceId}/likes —> increments likes and returns { "likes": number }
  • DELETE /api/resources/{resourceId}/likes —> decrements likes and returns { "likes": number }

Create a Svelte application that meets the following requirements:

  • When a user navigates to a page with the path /resources/:resourceId, the application should fetch and display the current like count for that resource. The like count should be displayed using the format: “Likes for {resourceId}: {likes}”.

  • Each resource page should include two buttons:

    • “Increase likes”: When clicked, this button should send a POST request to the API to increment the like count for the resource. After the request, the displayed like count should be updated to reflect the new value returned by the API.
    • “Decrease likes”: When clicked, this button should send a DELETE request to the API to decrement the like count for the resource. After the request, the displayed like count should be updated to reflect the new value returned by the API.

The assignment template comes with the following set of files. Use them for your implementation:

  • lib/apis/likesApi.js: This is where you should implement the client-side API calls to interact with the server-side API.
  • lib/components/Likes.svelte: This component can be used to display the page-specific information like count and the buttons for increasing and decreasing likes.
  • routes/resources/[resourceId]/+page.svelte: This file should use the dynamic parameter and the Likes component to implement the required functionality.