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.

Loading Exercise...

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 };
Loading Exercise...

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.


Loading Exercise...

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.
Loading Exercise...

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>
Loading Exercise...

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.
Loading Exercise...