JavaScript has grown significantly over the past few decades, evolving from a simple scripting language for enhancing web pages to a robust, multifaceted programming language that powers a large portion of the web. With this evolution, the tools and techniques used for making HTTP requests have also grown and diversified. One of the most common ways to make HTTP requests in JavaScript is by using the fetch
API. While fetch
is a powerful and flexible tool, it’s not without its flaws. In fact, many developers find it cumbersome and error-prone in complex scenarios. Enter Ky
, a modern, user-friendly library that aims to solve many of the pain points associated with fetch
.
In this comprehensive guide, we’ll explore why fetch
might not be the best choice for making HTTP requests and why Ky
is a better alternative. We’ll cover everything from basic use cases to advanced scenarios, highlighting the strengths and advantages of using Ky
. By the end of this guide, you’ll have a thorough understanding of why Ky
is a superior choice and how to seamlessly integrate it into your projects. Let’s dive in!
Table of Contents
- Why Does Fetch Suck?
- What is Ky?
- Ky vs. Fetch: A Detailed Comparison
- Installation and Getting Started with Ky
- Basic Usage of Ky
- Handling HTTP Errors with Ky
- Advanced Features of Ky
- How to Use Ky in Node.js
- Using Ky with React and Other Frameworks
- Testing with Ky
- Integrating Ky into Legacy Projects
- Performance and Optimization with Ky
- Real-World Use Cases for Ky
- Ky Plugins and Extending Functionality
- Best Practices for Using Ky
- Common Pitfalls and How to Avoid Them
- Ky vs. Axios: Which One Should You Use?
- Conclusion: Why Ky is a Better Choice for Modern JavaScript Development
1. Why Does Fetch Suck?
Before we explore the benefits of Ky, it’s essential to understand why the fetch
API often falls short. While fetch
is a significant improvement over the older XMLHttpRequest
method, it’s far from perfect. Here are some of the reasons why fetch
can be frustrating to work with:
1.1 Lack of Automatic Timeout Handling
One of the biggest drawbacks of fetch
is that it doesn’t support timeouts natively. If a request takes longer than expected, fetch
will keep waiting indefinitely unless you manually implement a timeout mechanism using Promise.race
or a similar approach. This can lead to potential memory leaks and unexpected behavior in production applications.
1.2 No Built-In Support for Aborting Requests
While the introduction of AbortController
has somewhat addressed this issue, it’s still not as straightforward as it should be. Aborting requests requires additional boilerplate code, making it cumbersome to implement.
1.3 No Built-In Handling of JSON and Other Data Formats
With fetch
, you have to manually parse the response using methods like response.json()
or response.text()
. This can lead to errors if the content type isn’t as expected or if the parsing fails. Handling different content types requires additional logic, making the code more complex.
1.4 Complex Error Handling
Error handling in fetch
is notoriously tricky. Unlike other libraries, a fetch
request will not reject on HTTP errors (like 404 or 500). This means you have to check the response.ok
property manually for each request, leading to verbose and error-prone code.
1.5 Verbose Syntax
For simple requests, fetch
can be fine, but as soon as you start dealing with headers, body content, and other options, the syntax becomes verbose and hard to manage. This verbosity often leads to bugs and makes the code less readable.
Here’s a quick example of how cumbersome fetch
can become:
fetch("https://api.example.com/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
body: JSON.stringify({ name: "John Doe", age: 30 }),
})
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok: " + response.statusText);
}
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error("Fetch error: ", error));
1.6 Lack of Interceptor Support
Unlike other libraries like Axios, fetch
doesn’t have a built-in mechanism for intercepting requests and responses. This makes it challenging to implement features like authentication token injection, request retries, or custom logging.
1.7 Boilerplate Code for Repeated Patterns
Common patterns, such as setting headers, handling errors, and transforming responses, require you to repeat similar code snippets for each request. This leads to a lot of boilerplate and duplicated code, which can be difficult to maintain.
These are just a few of the reasons why fetch
is often considered a suboptimal choice for complex applications. While it’s a good starting point for basic requests, it quickly becomes unwieldy as your project grows in complexity.
2. What is Ky?
Ky is a lightweight HTTP client for browsers and Node.js that provides a more ergonomic and developer-friendly API compared to fetch
. It’s built on top of the fetch
API but abstracts away many of its quirks and limitations, offering a more intuitive and powerful interface.
2.1 Key Features of Ky
- Promise-Based API: Ky uses Promises, making it easy to work with asynchronous requests.
- Automatic JSON Parsing: Ky automatically handles JSON parsing, so you don’t have to manually call
.json()
on the response. - Timeout Support: Ky includes built-in timeout support, making it easy to avoid hanging requests.
- Aborting Requests: Aborting requests is simple and straightforward with Ky, thanks to its use of the native
AbortController
. - Retry Mechanism: Ky has built-in retry support, allowing you to configure how many times a request should be retried if it fails.
- Customizable Hooks: You can define hooks for requests and responses, making it easy to implement features like logging, authentication, and more.
- Browser and Node.js Support: Ky works seamlessly in both browser and Node.js environments.
2.2 Why Choose Ky Over Fetch?
The primary reason to choose Ky over fetch
is simplicity. Ky reduces the amount of boilerplate code you need to write, handles common scenarios like timeouts and retries out-of-the-box, and provides a cleaner API for making requests. If you’ve ever struggled with fetch
, switching to Ky can make your life a lot easier.
3. Ky vs. Fetch: A Detailed Comparison
To illustrate why Ky is superior to fetch
, let’s look at some side-by-side comparisons of common use cases.
3.1 Making a Simple GET Request
With fetch
:
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Fetch error: ", error));
With Ky:
import ky from "ky";
ky.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error));
Ky’s syntax is cleaner and more intuitive. The .json()
method is part of the Ky library, eliminating the need for response.json()
calls.
3.2 Handling Timeouts
With fetch
, handling timeouts is cumbersome:
const fetchWithTimeout = (url, options, timeout = 5000) => {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timed out")), timeout)
),
]);
};
fetchWithTimeout("https://api.example.com/data")
.then((response) => response.json())
.catch((error) => console.error(error.message));
With Ky, timeouts are built-in:
ky.get("https://api.example.com/data", { timeout: 5000 })
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error.message));
3.3 Retrying Failed Requests
fetch
has no built-in support for retries, so you need to implement it manually:
const fetchWithRetry = (url, options, retries = 3) => {
const fetchData = (attempt) => {
return fetch(url, options).catch((error) => {
if (attempt <= retries) {
return fetchData(attempt + 1);
}
throw error;
});
};
return fetchData(1);
};
fetchWithRetry("https://api.example.com/data")
.then((response) => response.json())
.catch((error) => console.error(error.message));
With Ky, retries are easy to configure:
ky.get("https://api.example.com/data", { retry: 3 })
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error
: ", error.message));
3.4 Handling JSON and Other Response Types
fetch
requires manual handling for different content types:
fetch("https://api.example.com/data")
.then((response) => {
if (response.headers.get("content-type")?.includes("application/json")) {
return response.json();
}
return response.text();
})
.then((data) => console.log(data))
.catch((error) => console.error(error.message));
With Ky, response handling is simplified:
ky.get("https://api.example.com/data")
.text()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error.message));
3.5 Aborting Requests
Handling aborted requests in fetch
can be quite convoluted, involving the use of AbortController
. While it’s powerful, it’s not very intuitive, especially for developers who are new to the concept.
With fetch
, you need to create and use an AbortController
like this:
const controller = new AbortController();
const signal = controller.signal;
fetch("https://api.example.com/data", { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Fetch request was aborted");
} else {
console.error("Fetch error: ", error);
}
});
// Aborting the request
controller.abort();
With Ky, handling aborts is much simpler because the abort logic is directly integrated:
import ky from "ky";
const controller = new AbortController();
ky.get("https://api.example.com/data", { signal: controller.signal })
.json()
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Ky request was aborted");
} else {
console.error("Ky error: ", error);
}
});
// Aborting the request
controller.abort();
Ky doesn’t eliminate the need for AbortController
but makes it more intuitive to implement. The ability to pass the signal
directly into Ky’s options without extra boilerplate code makes it easier to handle such scenarios.
3.6 Using Ky with RESTful APIs
One of the areas where Ky really shines is when working with RESTful APIs. REST APIs often require repeated patterns for GET, POST, PUT, and DELETE requests, as well as handling various HTTP headers and request configurations. Ky simplifies these operations by offering methods specifically designed for REST operations, such as .get
, .post
, .put
, and .delete
.
Let’s explore how Ky can streamline interactions with RESTful APIs:
With fetch
:
fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
body: JSON.stringify({ name: "John Doe", age: 30 }),
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to create user: " + response.statusText);
}
return response.json();
})
.then((data) => console.log("User created: ", data))
.catch((error) => console.error("Fetch error: ", error));
With Ky:
ky.post("https://api.example.com/users", {
json: { name: "John Doe", age: 30 },
headers: {
Authorization: "Bearer token",
},
})
.json()
.then((data) => console.log("User created: ", data))
.catch((error) => console.error("Ky error: ", error));
Notice how Ky’s json
option automatically handles content-type headers and JSON stringification, reducing the amount of boilerplate code needed.
3.7 Ky’s Built-In Hooks for Request and Response Manipulation
One of Ky’s standout features is its built-in support for request and response hooks. These hooks allow you to intercept and manipulate requests and responses globally or for individual requests. This feature is useful for scenarios such as adding authentication tokens to headers, logging, or modifying response data before it reaches the calling function.
With fetch
, adding a custom header or transforming the response would require manually handling each request:
const fetchWithAuth = (url, options = {}) => {
const headers = {
...options.headers,
Authorization: "Bearer token",
};
return fetch(url, { ...options, headers }).then((response) => {
if (!response.ok) {
throw new Error("Fetch error: " + response.statusText);
}
return response.json();
});
};
fetchWithAuth("https://api.example.com/data")
.then((data) => console.log(data))
.catch((error) => console.error(error));
With Ky’s hooks, you can implement this once and reuse it across your entire project:
import ky from "ky";
const api = ky.extend({
hooks: {
beforeRequest: [
(request) => {
request.headers.set("Authorization", "Bearer token");
},
],
},
});
api.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error));
By using Ky’s extend
feature, you can define common hooks and configurations that can be shared across multiple requests, making it easier to implement cross-cutting concerns like authentication and logging.
3.8 Built-In Retry Mechanism
One of Ky’s most compelling features is its built-in retry mechanism. When dealing with unreliable networks or APIs, implementing a retry mechanism manually can be tedious and error-prone. Ky’s retry support is simple to configure and helps ensure your application remains resilient.
With fetch
, retrying failed requests involves creating a custom function:
const fetchWithRetry = (url, options, retries = 3) => {
const attemptFetch = (retryCount) => {
return fetch(url, options).catch((error) => {
if (retryCount <= 1) {
throw error;
}
console.log(`Retrying... (${retries - retryCount + 1}/${retries})`);
return attemptFetch(retryCount - 1);
});
};
return attemptFetch(retries);
};
fetchWithRetry("https://api.example.com/data", {}, 3)
.then((response) => response.json())
.catch((error) => console.error("Fetch error: ", error));
With Ky, retrying is a matter of setting the retry
option:
import ky from "ky";
ky.get("https://api.example.com/data", { retry: 3 })
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error));
You can also configure the retry mechanism with options such as methods
(which HTTP methods should be retried), statusCodes
(which response status codes should trigger a retry), and maxRetryAfter
(maximum wait time between retries).
3.9 Managing Query Parameters
Managing query parameters in fetch
can be cumbersome, especially when you have multiple parameters that need to be dynamically constructed. With fetch
, you often end up manually building the query string or using third-party libraries like qs
:
const params = { page: 1, limit: 10, search: "Ky library" };
const queryString = new URLSearchParams(params).toString();
fetch(`https://api.example.com/data?${queryString}`)
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Fetch error: ", error));
With Ky, query parameters are handled natively using the searchParams
option:
import ky from "ky";
ky.get("https://api.example.com/data", { searchParams: { page: 1, limit: 10, search: "Ky library" } })
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Ky error: ", error));
This approach is cleaner and eliminates the need for manually constructing query strings, reducing the risk of errors.
3.10 Extending Ky for Custom Configurations
Ky’s extend
method allows you to create customized instances with pre-configured options, making it easy to build reusable clients for different APIs. This is especially useful if you have multiple APIs in your project that require distinct configurations, such as separate base URLs or authentication headers.
For example, creating separate clients for a public and private API:
import ky from "ky";
const publicApi = ky.create({
prefixUrl: "https://api.public.com",
timeout: 5000,
});
const privateApi = ky.create({
prefixUrl: "https://api.private.com",
timeout: 5000,
headers: {
Authorization: "Bearer private_token",
},
});
publicApi.get("data")
.json()
.then((data) => console.log("Public API Data: ", data))
.catch((error) => console.error("Public API Error: ", error));
privateApi.get("user")
.json()
.then((data) => console.log("Private API Data: ", data))
.catch((error) => console.error("Private API Error: ", error));
This configuration flexibility allows you to tailor each client to specific use cases, keeping your code clean and maintainable.
4. Installation and Getting Started with Ky
4.1 Installing Ky
To get started with Ky, you first need to install it using your package manager of choice:
npm install ky
Or, if you prefer Yarn:
yarn add ky
After installation, you can start using Ky in your projects by importing it at the top of your JavaScript files:
import ky from "ky";
4.2 Making Your First Request with Ky
Let’s create a simple example to fetch data from a public API using Ky:
import ky from "ky";
ky.get("https://jsonplaceholder.typicode.com/posts")
.json()
.then((data) => console.log("Fetched Data: ", data))
.catch((error) => console.error("Error: ", error));
This straightforward example demonstrates the simplicity and ease of use that Ky brings to making HTTP requests. No need to manually parse JSON, handle errors, or worry about common issues like missing headers.
5. Handling HTTP Errors with Ky
Error handling is one of the critical components of working with HTTP requests. When using the fetch
API, error handling is cumbersome and can lead to unexpected issues. For example, a failed network request or a non-200 HTTP status code does not automatically throw an error, making it easy to overlook error conditions. Ky, on the other hand, offers robust and intuitive error handling mechanisms that simplify how you manage errors.
5.1 Automatic Error Handling in Ky
By default, Ky automatically throws an error if the response status code is not in the range of 200–299. This behavior is different from fetch
, where you must manually check for response status codes. With Ky, you get a more intuitive and safe approach to error handling:
import ky from "ky";
ky.get("https://api.example.com/invalid-endpoint")
.json()
.then((data) => console.log(data))
.catch((error) => {
console.error("Request failed with error: ", error);
console.error("Status code: ", error.response.status);
});
In the example above, Ky automatically throws an error when the response is not successful. The error object includes the response
property, making it easy to access the status code and other response details.
5.2 Customizing Error Handling
You can also customize error handling using the throwHttpErrors
option. By default, this option is set to true
, but you can disable it if you prefer to handle HTTP errors manually:
import ky from "ky";
ky.get("https://api.example.com/invalid-endpoint", { throwHttpErrors: false })
.then((response) => {
if (!response.ok) {
console.error("Failed request, status code: ", response.status);
} else {
return response.json();
}
})
.then((data) => console.log(data))
.catch((error) => console.error("Unexpected error: ", error));
In this example, Ky does not automatically throw an error when the status code indicates a failure. Instead, you can manually check the response.ok
property and handle errors as needed.
5.3 Working with HTTP Status Codes
Ky provides a simple way to filter and handle responses based on HTTP status codes. This can be useful when you want to implement specific logic for different types of responses, such as handling 404 Not Found
or 500 Internal Server Error
differently.
Here’s an example of how to handle specific status codes:
import ky from "ky";
ky.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch((error) => {
switch (error.response.status) {
case 404:
console.error("Resource not found");
break;
case 500:
console.error("Internal server error");
break;
default:
console.error("An unexpected error occurred: ", error);
}
});
By using the status
property on the error object, you can implement custom error-handling logic tailored to different response codes.
5.4 Accessing Response Body in Error Handlers
Sometimes, you may want to access the response body to get more information about the error. Ky makes this easy by allowing you to read the response body within the error handler:
import ky from "ky";
ky.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch(async (error) => {
const errorBody = await error.response.json();
console.error("Error details: ", errorBody);
});
In the example above, the error.response
property gives you access to the full response object, allowing you to parse and read the response body as needed.
6. Advanced Features of Ky
One of the main reasons to switch from fetch
to Ky is the abundance of advanced features that Ky offers. These features make it easier to work with complex HTTP request scenarios, such as timeouts, retries, hooks, and streaming.
6.1 Handling Timeouts
Timeouts are a common requirement for network requests, especially when dealing with unreliable networks or slow APIs. Ky makes it easy to set a timeout for your requests using the timeout
option:
import ky from "ky";
ky.get("https://api.example.com/data", { timeout: 5000 })
.json()
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "TimeoutError") {
console.error("The request timed out");
} else {
console.error("Unexpected error: ", error);
}
});
In the above example, if the request takes longer than 5000 milliseconds (5 seconds), Ky automatically throws a TimeoutError
. You can customize the timeout duration based on your requirements.
6.2 Request and Response Hooks
Hooks allow you to run custom logic before a request is sent and after a response is received. This is useful for tasks like logging, injecting authentication headers, or modifying the request and response data.
Example: Adding a Logging Hook
import ky from "ky";
const api = ky.create({
hooks: {
beforeRequest: [
(request) => {
console.log(`Making request to ${request.url} with method ${request.method}`);
},
],
afterResponse: [
(request, options, response) => {
console.log(`Received response with status ${response.status} from ${request.url}`);
},
],
},
});
api.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Error: ", error));
Hooks are defined in the hooks
object and allow you to intercept and modify requests and responses. This feature is particularly useful when implementing cross-cutting concerns like request logging, analytics, or custom header injection.
Example: Adding an Authentication Token
import ky from "ky";
const apiWithAuth = ky.create({
hooks: {
beforeRequest: [
(request) => {
request.headers.set("Authorization", `Bearer ${localStorage.getItem("token")}`);
},
],
},
});
apiWithAuth.get("https://api.example.com/protected")
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Error: ", error));
In this example, the beforeRequest
hook adds an Authorization
header with a bearer token to each request. This approach is much cleaner than manually adding headers for each individual request.
6.3 Extending Ky for Reusability
One of Ky’s strengths is its ability to create reusable configurations using the create
method. This method allows you to define common configurations, such as base URLs, headers, and hooks, which can then be shared across multiple requests.
import ky from "ky";
const api = ky.create({
prefixUrl: "https://api.example.com",
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
timeout: 5000,
});
api.get("data")
.json()
.then((data) => console.log("Data: ", data))
.catch((error) => console.error("Error: ", error));
api.post("users", { json: { name: "John Doe" } })
.json()
.then((data) => console.log("User created: ", data))
.catch((error) => console.error("Error: ", error));
In the above example, the api
instance is configured with a prefixUrl
, common headers, and a timeout. This reusable configuration can be used for all requests made using this instance, reducing boilerplate code and improving maintainability.
6.4 Streaming with Ky
While Ky is designed primarily for making HTTP requests, it also supports streaming large responses using the native Response
object. This is useful for scenarios where you need to handle large files or continuous data streams.
Here’s an example of how to handle streaming responses:
import ky from "ky";
const streamResponse = async () => {
const response = await ky.get("https://api.example.com/large-file", { responseType: "arrayBuffer" });
const data = new Uint8Array(await response.arrayBuffer());
console.log("Received streamed data: ", data);
};
streamResponse().catch((error) => console.error("Streaming error: ", error));
This approach allows you to work with streamed data in real-time, making it ideal for use cases such as downloading large files or handling continuous updates from a server.
7. Using Ky in Node.js
Although Ky is primarily designed for use in the browser, it also supports Node.js environments. However, you’ll need to install a fetch
polyfill because Node.js does not natively support the fetch
API.
7.1 Setting Up Ky in Node.js
To use Ky in a Node.js project, start by installing the node-fetch
library:
npm install node-fetch
Then, import node-fetch
and set it as the global fetch
function:
import ky from "ky";
import fetch from "node-fetch";
globalThis.fetch = fetch;
// Now you can use Ky as usual
ky.get("https://api.example.com/data")
.json()
.then((data) => console.log(data))
.catch((error) => console.error("Node.js Ky error: ", error));
This setup allows you to use Ky in server-side applications, making it a versatile choice for both frontend and backend projects.
11. Performance and Optimization with Ky
Performance is a critical factor in modern web development. Every HTTP request adds latency to your application, which can affect user experience, especially in data-intensive applications. One of Ky’s strengths is its ability to optimize HTTP requests and reduce overhead through built-in features such as retries, caching, and minimizing boilerplate code. Let’s explore how Ky can be leveraged to enhance the performance of your applications.
11.1 Built-In Caching for Repeat Requests
One of the primary ways to optimize HTTP requests is by using caching mechanisms to avoid fetching the same data multiple times. While Ky does not include a caching mechanism by default, it can easily be combined with caching libraries or service workers to implement caching strategies.
For example, you can use Ky in conjunction with a library like localforage
or the native Cache
API to cache responses and reuse them when appropriate:
import ky from "ky";
// Create a caching function using the Cache API
const cachedKy = async (url, options) => {
const cache = await caches.open("ky-cache");
const cachedResponse = await cache.match(url);
if (cachedResponse) {
return cachedResponse.json();
}
const response = await ky.get(url, options);
await cache.put(url, response.clone());
return response.json();
};
// Fetch data and cache it
cachedKy("https://api.example.com/data")
.then((data) => console.log("Fetched data: ", data))
.catch((error) => console.error("Error fetching data: ", error));
In this example, Ky’s response is stored in the browser’s cache after the first request. Subsequent requests to the same URL will use the cached response instead of making a new network request, reducing latency and improving performance.
11.2 Reducing Payload Size
Ky makes it easy to customize headers and compress payloads to reduce the size of HTTP requests and responses. This is particularly useful when dealing with large data sets or when optimizing for slow networks.
Example: Sending Compressed JSON Data
import ky from "ky";
// Prepare a large JSON object
const largeData = {
name: "John Doe",
age: 30,
hobbies: ["coding", "reading", "hiking"],
// ...more fields
};
// Compress the JSON data using Gzip (using an external library like pako)
import pako from "pako";
const compressedData = pako.gzip(JSON.stringify(largeData));
// Send the compressed data
ky.post("https://api.example.com/submit", {
body: compressedData,
headers: {
"Content-Encoding": "gzip",
"Content-Type": "application/json",
},
})
.json()
.then((response) => console.log("Response: ", response))
.catch((error) => console.error("Error: ", error));
By sending compressed data, you can significantly reduce the size of the payload, leading to faster transfers and lower bandwidth usage.
11.3 Using Prefetching and Preloading for Improved Perceived Performance
Ky can be integrated with prefetching and preloading strategies to improve the perceived performance of your web application. By prefetching data when the user is likely to need it (e.g., when hovering over a link or when the user is idle), you can ensure that data is already available when the user navigates to a new page.
Here’s an example of prefetching data with Ky:
import ky from "ky";
let userDataCache = null;
// Prefetch user data
const prefetchUserData = () => {
ky.get("https://api.example.com/user")
.json()
.then((data) => {
userDataCache = data;
console.log("User data prefetched");
})
.catch((error) => console.error("Error prefetching user data: ", error));
};
// Call prefetch when the user hovers over the profile link
document.getElementById("profile-link").addEventListener("mouseover", prefetchUserData);
// Use prefetched data when available
const showUserProfile = () => {
if (userDataCache) {
console.log("Using cached user data: ", userDataCache);
} else {
ky.get("https://api.example.com/user")
.json()
.then((data) => console.log("Fetched user data: ", data))
.catch((error) => console.error("Error fetching user data: ", error));
}
};
This approach ensures that the data is already loaded when the user navigates to the profile page, making the application feel faster and more responsive.
11.4 Minimize Unnecessary Requests
Using Ky’s built-in request and response hooks, you can implement logic to prevent duplicate or unnecessary requests. For example, if the application makes frequent requests to the same URL within a short period, you can cancel redundant requests or use previously fetched data to respond faster.
Here’s an example of using hooks to avoid duplicate requests:
import ky from "ky";
let inProgressRequests = new Map();
const api = ky.create({
hooks: {
beforeRequest: [
(request) => {
if (inProgressRequests.has(request.url)) {
throw new Error("Duplicate request detected: " + request.url);
}
inProgressRequests.set(request.url, true);
},
],
afterResponse: [
(request) => {
inProgressRequests.delete(request.url);
},
],
},
});
// Fetch data while avoiding duplicates
api.get("https://api.example.com/data")
.json()
.then((data) => console.log("Data fetched: ", data))
.catch((error) => console.error("Error: ", error));
This implementation uses a Map
to keep track of in-progress requests and prevents duplicate requests to the same URL. This can be particularly useful in scenarios where the same component is rendered multiple times or when user actions trigger redundant HTTP requests.
11.5 Optimizing Response Handling with Streaming
For large responses, using Ky’s stream
option can improve performance by processing data as it is received instead of waiting for the entire response to load. This is particularly beneficial when dealing with large files, video content, or continuous data streams.
Example: Streaming a Large JSON File
import ky from "ky";
const processLargeData = async () => {
const response = await ky.get("https://api.example.com/large-data", {
responseType: "text",
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunk = decoder.decode(value || new Uint8Array(), { stream: true });
console.log("Received chunk: ", chunk);
}
console.log("Finished streaming data");
};
processLargeData().catch((error) => console.error("Streaming error: ", error));
By streaming the response, you can process and display chunks of data as they arrive, reducing the memory overhead and improving responsiveness.
12. Real-World Use Cases for Ky
Ky’s rich feature set makes it suitable for a variety of real-world applications, ranging from simple data fetching to complex data aggregation and streaming. Let’s look at some practical scenarios where Ky excels:
12.1 Building a Data-Driven Dashboard
In data-intensive applications like dashboards, Ky’s performance and error-handling features can be leveraged to build a robust data-fetching layer. Consider a dashboard that needs to fetch multiple metrics from different APIs and update them in real-time.
import ky from "ky";
const fetchMetrics = async () => {
try {
const [users, orders, revenue] = await Promise.all([
ky.get("https://api.example.com/users/metrics").json(),
ky.get("https://api.example.com/orders/metrics").json(),
ky.get("https://api.example.com/revenue/metrics").json(),
]);
return { users, orders, revenue };
} catch (error) {
console.error("Error fetching metrics: ", error);
return { users: 0, orders: 0, revenue: 0 };
}
};
fetchMetrics().then((metrics) => console.log("Dashboard Metrics: ", metrics));
In this example, Ky’s concurrent request handling and built-in error handling ensure that all metrics are fetched efficiently, even if one of the APIs fails.
12.2 Creating a Secure API Gateway with Rate Limiting
If you’re building a backend API gateway that interacts with multiple third-party services, Ky’s hooks and retry mechanisms make it easy to implement features like rate limiting, authentication, and request logging.
import ky from "ky";
import fetch from "node-fetch";
globalThis.fetch = fetch;
const gatewayClient = ky.create({
prefixUrl: "https://api.gateway.com",
headers: {
"X-Client-ID": process.env.CLIENT_ID,
},
hooks: {
beforeRequest: [
(request) => {
console.log(`Requesting: ${request.url}`);
},
],
afterResponse: [
(request, options, response) => {
console.log(`Response: ${response.status} from ${request.url}`);
},
],
},
});
// Proxy a request through the gateway
gatewayClient.get("service/endpoint")
.json()
.then((data) => console.log("API Gateway Response: ", data))
.catch((error) => console.error("Gateway Error: ", error));
This example configures an API gateway client with custom headers and logging hooks. It serves as a foundation for
building more advanced features, such as rate limiting and request throttling.
13. Ky Plugins and Extending Functionality
One of the most powerful features of Ky is its extensibility through plugins. While Ky comes with a robust set of built-in features, plugins allow you to further enhance its functionality by adding custom behaviors, middleware, or integrations.
13.1 Creating a Custom Plugin for Ky
Let’s create a custom plugin that adds a custom header to all requests for audit tracking purposes.
import ky from "ky";
// Define the custom plugin
const auditPlugin = {
beforeRequest: [
(request) => {
request.headers.set("X-Audit-Track", "User1234");
},
],
};
// Create a Ky instance with the plugin
const auditClient = ky.create({
hooks: auditPlugin,
});
// Use the custom client
auditClient.get("https://api.example.com/data")
.json()
.then((data) => console.log("Audited Response: ", data))
.catch((error) => console.error("Error: ", error));
This plugin adds an X-Audit-Track
header to each request, allowing you to track user activity for audit purposes. You can build more sophisticated plugins to handle a variety of tasks, such as transforming responses, modifying requests based on conditions, or integrating with third-party logging services.
14. Best Practices for Using Ky
Ky is a versatile and powerful library, but to get the most out of it, it’s important to follow best practices when implementing it in your projects. These practices will help you write clean, maintainable, and efficient code, ensuring that your HTTP interactions are robust and resilient to change. Below, we’ll discuss some best practices to keep in mind when using Ky in both frontend and backend applications.
14.1 Using ky.create()
for Modular API Configurations
One of the most effective ways to manage multiple API endpoints in your application is by using ky.create()
to configure and reuse Ky instances. This approach is particularly useful when working with different environments (e.g., development, staging, production) or when interacting with multiple third-party services.
Example: Creating Modular API Clients
import ky from "ky";
// Create separate instances for different services
const userApi = ky.create({
prefixUrl: "https://api.example.com/users",
headers: {
Authorization: "Bearer user_token",
},
});
const orderApi = ky.create({
prefixUrl: "https://api.example.com/orders",
headers: {
Authorization: "Bearer order_token",
},
});
// Use the configured clients
userApi.get("profile")
.json()
.then((data) => console.log("User Profile: ", data))
.catch((error) => console.error("User API Error: ", error));
orderApi.post("new", { json: { itemId: 123, quantity: 2 } })
.json()
.then((data) => console.log("Order Created: ", data))
.catch((error) => console.error("Order API Error: ", error));
By using separate instances for each API, you can manage different configurations, authentication tokens, and headers in a clean and maintainable way.
14.2 Using Environment Variables for Sensitive Data
Hardcoding API keys, tokens, or URLs directly in your code can lead to security vulnerabilities and configuration headaches when moving between environments. Instead, store sensitive information in environment variables and reference them in your Ky configurations.
import ky from "ky";
const apiClient = ky.create({
prefixUrl: process.env.API_BASE_URL,
headers: {
"API-Key": process.env.SECURE_API_KEY,
},
});
apiClient.get("data")
.json()
.then((data) => console.log("Data: ", data))
.catch((error) => console.error("API Error: ", error));
In this example, the base URL and API key are read from environment variables, ensuring that sensitive information is not exposed in the source code. This practice also makes it easier to switch configurations based on the deployment environment (e.g., development vs. production).
14.3 Centralize Error Handling with Ky Hooks
When building complex applications, you may want to handle errors in a centralized way. Instead of duplicating error-handling logic across multiple components or modules, consider using Ky’s hooks
to define global error handlers.
Example: Global Error Logging with a Ky Hook
import ky from "ky";
// Create a reusable Ky instance with global error handling
const apiClient = ky.create({
hooks: {
afterResponse: [
async (request, options, response) => {
if (!response.ok) {
console.error(`Request failed: ${response.status} - ${response.statusText}`);
const errorData = await response.json();
console.error("Error details: ", errorData);
}
},
],
},
});
// Use the client as usual
apiClient.get("https://api.example.com/data")
.json()
.then((data) => console.log("Data: ", data))
.catch((error) => console.error("Handled Error: ", error));
By defining a global error handler in the afterResponse
hook, you can log errors consistently and even implement features like error notifications or retry strategies in one place.
14.4 Managing Timeouts for Different Scenarios
Not all HTTP requests require the same timeout duration. For example, requests to a local API might need shorter timeouts, while calls to a remote server might need a longer duration. Use Ky’s timeout
option to configure different timeout values for various scenarios.
Example: Customizing Timeouts for Different Endpoints
import ky from "ky";
const shortTimeoutApi = ky.create({
prefixUrl: "https://api.localservice.com",
timeout: 2000, // 2 seconds timeout for local service
});
const longTimeoutApi = ky.create({
prefixUrl: "https://api.remoteservice.com",
timeout: 10000, // 10 seconds timeout for remote service
});
shortTimeoutApi.get("status")
.json()
.then((data) => console.log("Local Service Status: ", data))
.catch((error) => console.error("Local Service Error: ", error));
longTimeoutApi.get("data")
.json()
.then((data) => console.log("Remote Service Data: ", data))
.catch((error) => console.error("Remote Service Error: ", error));
By setting different timeout values, you can optimize the user experience based on the expected latency of each service.
14.5 Use Ky for Fetching, Axios for Complex Scenarios
If you’re working on an application that requires complex request configurations, such as custom request transformers, advanced interceptors, or multipart file uploads, consider using Ky alongside Axios. While Ky excels at providing a lightweight and easy-to-use HTTP client, Axios is better suited for certain advanced use cases.
For example, use Ky for lightweight data-fetching operations and Axios for more complex scenarios, such as large file uploads or interacting with APIs that require advanced configurations.
import ky from "ky";
import axios from "axios";
// Use Ky for data fetching
const fetchData = async () => {
try {
const data = await ky.get("https://api.example.com/data").json();
console.log("Fetched Data: ", data);
} catch (error) {
console.error("Ky Error: ", error);
}
};
// Use Axios for file uploads
const uploadFile = async (file) => {
const formData = new FormData();
formData.append("file", file);
try {
const response = await axios.post("https://api.example.com/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
console.log("File uploaded: ", response.data);
} catch (error) {
console.error("Axios Error: ", error);
}
};
Using both libraries in the same project allows you to leverage the strengths of each, making it easier to handle a wide range of HTTP request scenarios.
14.6 Secure Your Application by Avoiding Overexposure
When using Ky in frontend applications, be cautious about exposing sensitive URLs, headers, or tokens to the client. Consider moving sensitive API calls to a backend server where they can be securely handled. For example, instead of calling a third-party payment service directly from the frontend, use a backend proxy to handle the request and return the sanitized response to the client.
Example: Creating a Secure Proxy for Sensitive API Requests
import express from "express";
import ky from "ky";
import fetch from "node-fetch";
globalThis.fetch = fetch;
const app = express();
app.use(express.json());
app.post("/secure-payment", async (req, res) => {
try {
const paymentResponse = await ky.post("https://api.paymentservice.com/charge", {
json: req.body,
headers: {
Authorization: `Bearer ${process.env.PAYMENT_API_KEY}`,
},
}).json();
res.json(paymentResponse);
} catch (error) {
console.error("Payment Service Error: ", error);
res.status(500).send("Payment processing failed");
}
});
app.listen(3000, () => console.log("Server running on port 3000"));
In this example, the backend handles the sensitive payment processing using Ky. The frontend sends the request to the backend server, which then communicates with the third-party payment service. This setup reduces the risk of exposing sensitive data and credentials on the client side.
14.7 Use Ky’s .json()
, .text()
, and .blob()
Methods for Type-Safe Responses
When using Ky’s built-in methods for response parsing (.json()
, .text()
, .blob()
), make sure to utilize them appropriately based on the expected content type of the response. This approach improves type safety and ensures that your application handles different data formats correctly.
import ky from "ky";
// Handle JSON responses
const fetchJsonData = () =>
ky.get("https://api.example.com/data")
.json()
.then((data) => console.log("JSON Data: ", data))
.catch((error) => console.error("Error: ", error));
// Handle text responses
const fetchTextData = () =>
ky.get("https://api.example.com/text")
.text()
.then((data) => console.log("Text Data: ", data))
.catch((error) => console.error("Error: ", error));
// Handle binary data
const fetchBlobData = () =>
ky.get("https://api.example.com/file")
.blob()
.then((blob) => console.log("Blob Data: ", blob))
.catch((error) => console.error("Error: ", error));
Using the appropriate parsing method ensures that your application correctly interprets different content types and avoids runtime errors caused by incorrect parsing.
15. Common Pitfalls and How to Avoid Them
While Ky is
an excellent library, there are a few common pitfalls that developers can run into when using it. Let’s address some of the most common mistakes and how to avoid them.
15.1 Forgetting to Handle Non-JSON Responses
Ky’s .json()
method is convenient, but it assumes that the response is valid JSON. If the server returns a non-JSON response (e.g., an error message or HTML page), it can lead to unexpected errors. Always verify the Content-Type
header or use .text()
for non-JSON responses.
15.2 Overusing Global Hooks
Global hooks are powerful, but using too many can lead to unexpected behaviors, especially when multiple plugins or middlewares are involved. Be cautious about modifying the same request or response object in different hooks.
15.3 Ignoring Timeout Errors
Ky’s built-in timeout support is a great feature, but developers often overlook timeout errors. Ensure that you properly handle timeout scenarios to provide a better user experience, such as displaying a retry button or showing a custom error message.
Conclusion: Why Ky is a Better Choice for Modern JavaScript Development
After exploring the capabilities, advantages, and best practices of Ky, it’s clear that this library is a powerful tool for modern JavaScript development. Its simplicity, flexibility, and rich feature set make it a compelling alternative to the native fetch
API and even established libraries like Axios.
By adopting Ky, you gain access to features such as automatic retries, global hooks, and ergonomic request handling, which streamline the process of making HTTP requests and managing complex scenarios. Whether you’re building a small single-page application or a large enterprise-grade platform, Ky can simplify your code, reduce boilerplate, and make your HTTP interactions more robust.
So, if you’re tired of dealing with the quirks of fetch
or looking for a more lightweight and efficient alternative to other HTTP clients, Ky is a tool worth adding to your development toolkit. Happy coding!