This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.
Last reviewed: Dec '24
1. Introduction
The JavaScript "Fetch" function provides the software equivalent of your favourite hammer when you need to build something that calls a web address (conventionally referred to as an "endpoint") to:
- run an internal operation such as a routine data reorganisation, or
- despatch some data for processing by a third-party site- for example, a message to be circulated by a mailing site, or
- receive some data from a third party such as a weather-forecasting site.
When you're building a webapp with Svelte, you'll find you need "Fetch" on far fewer occasions than you would have in the past. This is because Svelte's built-in support for server-side operations through its load()
and actions()
integrations replaces those occasions when you would have had to make "Fetch" calls to access bits of your own site. Nevertheless, when you need it, you need it and "Fetch" will be fabulously useful to you.
2. The Asynchronous Javascript "Fetch" function
As mentioned briefly in Post 2.3, Javascript is an asynchronous programming language, and the "Fetch" function is exactly the sort of activity that triggers this behaviour. In such cases, the flow of activity from statement to statement doesn't halt when it encounters a statement that will take an indeterminate length of time to complete. Rather, it simply sidelines the statement into a parallel thread and ploughs on, assuming that the thread will register an "event" that will fire on completion and trigger an appropriate conclusion. I suggest that you might like, at this point, to ask chatGPT to give you a quick tutorial on the Javascript Event Loop!
As you may imagine, all of this can complicate a coder's life, so you won't be too surprised to learn that this aspect of the language has experienced intense development. I won't go into the history of all this as this would only confuse you. What I describe below simply presents the current state of play.
For most purposes, probably the most useful form of the "Fetch" function is one that uses the await
keyword to locally "conceal" Javascript's asynchronous behaviour while it performs a request that sends some data to a remote address. In return, it receives, in another awaited
statement, a response that returns the requested information. Each of these steps appears to behave synchronously and thus permits you to submit a series of such requests in strict sequence. Here's a "template" for such a "Fetch" application using the "POST" method:
async function performMyPostTask() {
try {
const response = await fetch(
urlForPost,
{
method: "POST", // HTTP method
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "foo",
body: "bar",
userId: 1,
}),
},
);
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error:", error);
}
}
Now, where do I begin explaining this? Let's start with the await
and async
keywords.
3. The Javascript await
and async
keywords
ECMA, the organisation responsible for the JavaScript standard, added the await
keyword to the language in 2017. Essentially, this makes a function call wait for a "promise" to "resolve" before proceeding to the next statement. The same ECMA standard also introduced an async
keyword that you use when declaring a function that uses await
to make it return a "promise".
Many programmers initially found these concepts challenging, but they’ve become a standard part of modern JavaScript workflows. Once you're used to the arrangement, you'll find yourself automatically using long sequences of await
calls as if each was a synchronous operation.
Just to put this into context then, the "Fetch" example above would typically be encountered in a button function such as the following example:
<button
on:click={async () => {
const data = await performMyPostTask();
console.log("data: " + JSON.stringify(data));
}}>Click Me</button
>
Of course, there will be times when you positively want to take advantage of Javascript's asynchronous behaviour and launch a "battery" of database calls that run in parallel. This can make a huge difference to the responsiveness of your webapp. Each call can be coded to return a promise that you tuck into an array. In turn, this can be monitored by a global function "awaiting" for them all to resolve. You can see an example of this arrangement in the "Reading Documents" section of Post 10.1.
An alternative to the async
and await
keywords is to use a .then
method subsequent to the call of an asynchronous function. This gives you more control of error handling arrangements but can become cumbersome when used to code long sequences of asynchronous operations. Personally, I've always preferred the await
method
The next thing you'll want to know is what this "POST" business is all about.
4. "Fetch" methods - "GET" and "POST"
A "Fetch" function's "Method" declares the mechanism that will be used to transfer data to the target endpoint. It's probably best to start with the arrangement used by the "GET" method. This arrangement is built into the browser so you can use a"GET" call without actually writing any Javascript. Here, the data being sent to the endpoint is coded directly into the URL used to "call" the endpoint. Here's an example (coded as a call to a demonstrator GET endpoint configured as a +server.js file in the local API folder):
http://localhost:5173/api/perform-my-get-task?taskSpec=taskSpecValue
Here's how you might use fetch()
to code this programmatically:
<script>
const urlForGet = "http://localhost:5173/api/perform-my-get-task";
const taskSpec = "taskSpecValue";
async function performMyGetTask() {
try {
const response = await fetch(urlForGet + "?taskSpec=" + taskSpec);
if (!response.ok) {
throw new Error("HTTP error! Status: " + response.status);
}
const data = await response.json(); // Parse JSON response
console.log("Task result:", data);
} catch (error) {
console.error("Error during fetch:", error);
}
}
callPerformMyGetTask();
</script>
Here's what the perform-my-get-task
endpoint might look like:
// src/routes/api/perform-my-get-task/+server.js
import { json, error } from '@sveltejs/kit';
export function GET({ url }) {
// Get the `taskSpec` query parameter
const taskSpec = url.searchParams.get('taskSpec');
if (!taskSpec) {
// Return a 400 error if the parameter is missing
throw error(400, { message: 'Missing taskSpec query parameter' });
}
// Simulate performing the task
console.log("Task received with spec: " + taskSpec);
const result = {
message: "Task " + taskSpec + " has been successfully performed",
timestamp: new Date().toISOString(),
};
// Return the result as a JSON response
return json(result);
}
The functional "payload" of the +server.js
file is "exported" here because SvelteKit requires server routes to expose specific handler functions (GET, POST, etc.) for incoming HTTP requests.
A little thought will awaken you to the limitations of GET - the parameters must be coded into a string, browsers impose a maximum length on the endpoint call string and the string is visible in the browser's URL field.
By contrast, the POST method minimises most of these limitations. Here, data is passed as an object, can be substantially larger and is no longer visible in the URL (though still not strictly secure).
Here's what a perform-my-post-task
endpoint might look like:
// src/routes/api/perform-my-post-task/+server.js
import { json, error } from '@sveltejs/kit';
export async function POST({ request }) {
try {
// Parse the JSON body from the incoming request
const body = await request.json();
// Log the received data for debugging purposes
console.log("Received body:", body);
// Simulate a response payload
const simulatedResponse = {
id: 101, // Simulated ID for the created resource
...body, // Echo back the received data
createdAt: new Date().toISOString(), // Add a timestamp for when the resource was created
};
// Return the response as JSON with a 200 OK status
return new Response(JSON.stringify(simulatedResponse), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error processing POST request:", error);
// Return a 500 Internal Server Error for unexpected issues
return new Response(
JSON.stringify({ error: "Internal Server Error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
5. "Fetch" in Svelte
The "Fetch" API only became available in server-side code in 2022. This was because server-side code runs in Node.js (as opposed to the JavaScript environment provided by web browsers), and early versions of Node.js didn't natively provide a "Fetch" function.
But even before that date, Svelte developers were insulated from the consequent problems because SvelteKit provided its own "polyfill" replacement. This meant that you could code server-side "Fetch" calls in exactly the same way as for client-side code.
This was a big help because the fall-back alternatives in Node.js equivalents of "Fetch" were third-party libraries like Axios or node-fetch. These were fine in themselves (and, indeed, Axios offers some useful features like automatic JSON parsing), but they required you to learn a different calling pattern (groan). SvelteKit's polyfill was a big help because it meant that you could use a consistent "Fetch" interface regardless of whether your code was running on the client or the server.
Even now that "Fetch" is formally available in Node.js, SvelteKit continues to double down on its original approach and provides a special version of "Fetch" that :
- Supports relative URLs, allowing you to make same-origin requests without needing fully qualified URLs.
- Automatically includes cookies from the incoming client request when making relative, same-origin requests. This makes it easy to provide session-based authentication.
Note that, for cross-origin requests, you’ll still need to manually manage headers like Authorization.
Top comments (0)