Client-Server Interaction

Fetch API and Client-Side Requests


Learning Objectives

  • You know of the Fetch API and you know how to interact with APIs in Svelte.
  • You know how to execute code when a component is mounted.

The Fetch API that is available in browsers and JavaScript runtimes is used to interact with server-side APIs. Here, we’ll first look into the Fetch API, after which we look into using the Fetch API in Svelte components.

Fetch API

Fetch API allows making HTTP requests programmatically from JavaScript code. The key method to use is fetch, which allows creating and executing the request, returning a Response object. The method is asynchronous, which means that we need to use the await keyword to wait for the asynchronous method to finish.

GET request

Making a GET request with the fetch method is done by passing the target URL to the method. In the following example, we make a GET request to an address, and then ask for the data JSON format using the json method of the response object.

const response = await fetch("https://wsd-todos-api.deno.dev/todos");
const jsonData = await response.json();
console.log(jsonData);

The above example makes a call to the address https://wsd-todos-api.deno.dev/todos, which provides a mock endpoint for interacting with todos. A GET request to the address leads to the server responding with a list of four todos.

When the above program is run with Deno, the output looks as follows.

deno run --allow-net app-test.js
[
  { id: 1, name: "Introduction to Web Applications", done: true },
  { id: 2, name: "Databases and Data Validation", done: true },
  { id: 3, name: "Client-Side Development", done: true },
  { id: 4, name: "Client-Server Interaction", done: false }
]

POST Request

When making a POST request with fetch, we give an address and an object as a parameter to the method. The object contains the properties headers, the request method, and the body. The body contains the data to be sent as a string. The following example outlines posting JSON data to the address that we previously used. In the example, we post a new todo object to the address and log the response.

const data = {
  name: "Evolution of Web Development",
  done: false,
};

const response = await fetch("https://wsd-todos-api.deno.dev/todos", {
  headers: {
    "Content-Type": "application/json",
  },
  method: "POST",
  body: JSON.stringify(data),
});

const jsonData = await response.json();
console.log(jsonData);

In the above example, we send a JSON object that contains the properties name and done. The JSON.stringify method is used to convert the JavaScript object to a JSON string.

Running the above example provides the following output.

deno run --allow-net app.js
{ name: "Evolution of Web Development", done: false, id: 5 }
Other request methods

Any type of request method can be used with the Fetch API, as we can define the method in the method property of the object that is passed to the fetch method.

Svelte and Fetch API

The Fetch API is a standard JavaScript API that can be used in any JavaScript environment, including Svelte. 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.

As an example, if the API root address would be https://https://wsd-todos-api.deno.dev/, we would define the address in the environment file as follows.

PUBLIC_API_URL = "https://https://wsd-todos-api.deno.dev/";

The address can then be imported to the components as follows.

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

Retrieving data

The following example outlines a component that can be used to make a GET request with fetch. The function makes a GET request to the path /todos/1 of the API, returning a single todo object in JSON format. Once the request has been made, the todo object is parsed into a JavaScript object, and assigned to the todo variable.

When the button on the page is pressed, the function is called. After the function finishes and the value of todo changes, the todo is shown on the page.

<script>
  import { PUBLIC_API_URL } from "$env/static/public";
  let todo = $state({});

  const getTodo = async () => {
    const response = await fetch(
      `${PUBLIC_API_URL}/todos/1`
    );
    todo = await response.json();
  };
</script>

<button onclick={getTodo}>Fetch todo</button>

<br />

<p>Todo is: {todo.name}</p>
Loading Exercise...

Posting data

Similarly, we could create a component for making a POST request with fetch. Below, the function addTodo makes a POST request to the path “/todos” of the API, which is used to add a new todo. The function would send a todo object to the address, and once the response is received, the id of the new todo is shown to the user.

<script>
  import { PUBLIC_API_URL } from "$env/static/public";
  let todo = $state({});

  const addTodo = async () => {
    const data = {
      name: "Evolution of Web Development",
      done: false,
    };

    const response = await fetch(`${PUBLIC_API_URL}/todos`, {
      headers: {
        "Content-Type": "application/json",
      },
      method: "POST",
      body: JSON.stringify(data),
    });

    todo = await response.json();
  };
</script>

<button onclick={addTodo}>Add todo</button>

<br />

<p>The id of the new todo is: {todo.id}</p>

While the above example demonstrates creating a POST request with static data, we could also create a form that allows the user to input the data.

Loading Exercise...

Separating API functionality

While the above examples include the fetch calls directly within the components, it is often beneficial to separate the API functionality from the components. This makes the components easier to read and maintain, and allows the API functionality to be reused in multiple components. Let’s look at this, modifying the earlier examples with a component for retrieving a todo and a component for adding a todo.

Create a folder called apis to the lib folder under src, if it does not yet exist. In the folder, create a new file called todos-api.js and place the following content to the file. The functions createTodo, readTodos, readTodo are variants of the functions in the above components.

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

const createTodo = async (name, done) => {
  const data = { name, done };

  const response = await fetch(`${PUBLIC_API_URL}/todos`, {
    headers: {
      "Content-Type": "application/json",
    },
    method: "POST",
    body: JSON.stringify(data),
  });

  return await response.json();
};

const readTodos = async () => {
  const response = await fetch(`${PUBLIC_API_URL}/todos`);
  return await response.json();
};

const readTodo = async (id) => {
  const response = await fetch(
    `${PUBLIC_API_URL}/todos/${id}`,
  );
  return await response.json();
};

export { createTodo, readTodos, readTodo };

Now, if we would wish to use one (or more) of the above functions in our component, we’d need to modify the component to use the new functionality. As an example, in the case of the component that is used for adding todos, the modified functionality would be as follows.

<script>
  import * as todosApi from "$lib/apis/todos-api.js";

  let todo = $state({});

  const addTodo = async () => {
    todo = await todosApi.createTodo("Refactor", false);
  };
</script>

<button onclick={addTodo}>Add todo</button>

<br />

<p>The id of the new todo is: {todo.id}</p>

Similarly, for reading a single todo, the component would be as follows.

<script>
  import * as todosApi from "$lib/apis/todos-api.js";

  let todo = $state({});

  const getTodo = async () => {
    todo = await todosApi.readTodo(1);
  };
</script>

<button onclick={getTodo}>Fetch todo</button>

<br />

<p>Todo is: {todo.name}</p>

Now, the components would be easier to read as the API functionality is separated from the component. Further, the API functionality could be reused in other components as well.

Effects and Fetch on Component Mount

Svelte has an effect rune $effect that can be used to execute code when a component is added to the page — or in other words, when the component is mounted to the DOM. The $effect rune is given a function that is be executed when the component is mounted.

As an example, if we would wish to load a list of todos when the component is mounted, we would implement it as follows.

<script>
  import * as todosApi from "$lib/apis/todos-api.js";

  let todos = $state([]);

  const getTodos = async () => {
    todos = await todosApi.readTodos();
  };

  $effect(() => {
    getTodos();
  });
</script>

<h1>Todos</h1>

<ul>
  {#each todos as todo}
    <li>{todo.name}</li>
  {/each}
</ul>

With the above, the todos are fetched from the API when the component is mounted. Once the todos have been fetched, they are shown to the client.

Loading Exercise...

Again, server-side rendering...

Similarly to the earlier discussion on server-side rendering when storing state locally, the above will lead to problems when server-side rendering is enabled. To avoid the issue, disable server-side rendering by adding a file called +layout.server.js to the routes folder (in the src folder) and placing the following line to it.

export const ssr = false;

Loading indicators

With the above example, we briefly see a moment with no todos when the todos have not yet been loaded. One possibility would be to provide a message to the user in such a scenario, and then change the message to the list of todos once the todos have been loaded. There are a few ways to achieve this, one of which is to create a separate state variable that is used to indicate whether the todos have been loaded or not. The following example outlines how this could be done.

<script>
  import * as todosApi from "$lib/apis/todos-api.js";

  let loading = $state(true);
  let todos = $state([]);

  const getTodos = async () => {
    loading = true;
    todos = await todosApi.readTodos();
    loading = false;
  };

  $effect(() => {
    getTodos();
  });
</script>

<h1>Todos</h1>

{#if loading}
  <p>Loading todos...</p>
{:else}
  <ul>
    {#each todos as todo}
      <li>{todo.name}</li>
    {/each}
  </ul>
{/if}

With the above, the message “Loading todos…” is shown to the user while the todos are being fetched. Once the todos have been fetched, the message is replaced with the list of todos.

Another possibility is to use Svelte’s await block to wait for the execution of asynchronous functionality. The await block is used to wait for the execution of a promise, and it can be used to show different content to the user while the promise is being resolved.

The await block consists of three parts. The first part is the #await block that includes the expression that is awaited. Inside the #await block, there are two blocks that are executed depending on whether the promise is resolved or rejected. For the resolved promise, there is a :then block that is executed, while for the rejected promise, there is a :catch block that is executed.

The following outlines the basic syntax of the await block.

{#await expression}
  show while waiting
{:then variable}
  show contents of the resolved promise from variable
{:catch variable}
  show if the promise is rejected
{/await}

The following example outlines how the todos would be listed with the await block. Note that the getTodos function is modified to return the promise instead of waiting for the promise to be resolved. Due to this, the function returns a promise, which is then awaited in the await block.

<script>
  import * as todosApi from "$lib/apis/todos-api.js";

  let todos = $state([]);

  const getTodos = async () => {
    todos = todosApi.readTodos();
  };

  $effect(() => {
    getTodos();
  });
</script>

<h1>Todos</h1>

{#await todos}
  <p>Loading todos...</p>
{:then todos}
  <ul>
    {#each todos as todo}
      <li>{todo.name}</li>
    {/each}
  </ul>
{/await}
Loading Exercise...