Published on October 21, 2023

Unstorage: An Async Key-Value Storage API for JavaScript

Unstorage: An Async Key-Value Storage API for JavaScript

I've just finished my third cup of coffee for the day, encouraging the thought, "There is no better time to write about Unstorage than now." So, here I am, writing about Unstorage.

Unstorage is an asynchronous Key-Value storage API packed with convenient features such as multi-driver mounting, built-in servers, and state snapshots. That's not all; Unstorage is designed with a tiny core, enabling compatibility with various ecosystems - the Browser, Node.js, Bun, Deno, Workers, etc.

The list of features goes on and on. It automatically handles JSON value serialization and deserialization and offers options for working with snapshots and metadata. It features default in-memory storage and tree-shaking utils to minimize clutter. As if these utilities weren't enough, Unstorage also includes HTTP Storage with a built-in server.

Even with all these options, it has a straightforward interface that is easy to learn and use. In this article, I'll be covering the basics of Unstorage and how you can use it in your projects. I also plan to write a follow-up article covering more advanced features.

Installation

Installing Unstorage is as simple as running the following command:

npm install unstorage

Alternatively, you can use Yarn or pnpm:

yarn add unstorage
pnpm add unstorage

Quick Start

With Unstorage installed, let's create a new storage and do some basic operations with it:

import { createStorage } from "unstorage";

// Create a new storage
const storage = createStorage();

await storage.setItem("foo", "bar");
await storage.getItem("foo"); // => "bar"
await storage.hasItem("foo"); // => true
await storage.removeItem("foo");

As you can see, it's straightforward to get started with Unstorage. The createStorage function returns a new storage instance that can be used to perform various operations on the storage. By default, Unstorage uses in-memory storage, but you can mount additional drivers to back your data to a filesystem, a database, or even a remote server. I will cover this in more detail later.

Also, remember that the storage instance is asynchronous, so all operations return a promise. Remembering this is a crucial point when working with Unstorage.

Detailed Overview of Unstorage's Interfaces

Unstorage provides an array of methods to interact with the storage. You can get a detailed overview of these by visiting their API documentation, but I'll cover the most important ones here with some examples.

For our example, let's assume we're developing an application that requires storing user data. We introduce unstorage to handle all our storage-related operations.

Storing User Data

For storing user data, we use the setItem function as follows:

await storage.setItem("user:details", {
name: "John Doe",
email: "john@doe.com",
});

We're using a key, user:details, to store user data. This pattern is commonly used in Unstorage to namespace keys. You can use any key you want, but it's recommended to use a namespace to avoid conflicts with other keys.

Notice how we passed an object as the second argument to setItem. Unstorage automatically serializes the value to JSON before storing it. This feature is convenient because it saves us from manually serializing the value.

Updating User Data

Say we want to update the user's email address. We will use the setItem function again to accomplish this:

await storage.setItem("user:details", {
name: "John Doe",
email: "john@doe.com",
});

Retrieving User Data

Now, to retrieve the stored user data, we use the getItem function:

const userData = await storage.getItem("user:details");
console.log(userData); // { name: "John Doe", email: "john@doe.com" }

Checking If An Item Exists

We can check if a key exists in our storage using the hasItem function:

const hasUserData = await storage.hasItem("user:details");
console.log(hasUserData); // true

Removing User Data

To remove a user's data, we use the removeItem function:

await storage.removeItem("user:details");

Getting All Keys

Suppose we want to get the keys of all the users stored in our system. We use the getKeys function for this:

const allKeys = await storage.getKeys();
console.log(allKeys); // [ 'user:details', ... ]

Clearing User Data

Now, if, for some reason, we want to remove all stored user data, clear everything, and start afresh, we use the clear function.

Be careful when using this function, as it will remove all data from the storage.

await storage.clear();

There are various other functions that you can use to interact with the storage. For example, you can use getItems and setItems to get and set multiple items at once. You can also use getItemRaw and setItemRaw to access and update the value of a key in its raw format, without deserialization to a primitive type. This feature is particularly useful for binary data.

Additionally, Unstorage provides commands to add, get, and update metadata related to a specific key, which is useful if, for example, you want to store the creation time of a file or any other custom data. You can also remove custom metadata related to a key.

Working with Types

You might want to ensure correct data types when storing or retrieving data. You can use unstorage with TypeScript to define types at the storage or individual key levels.

For example, let's define a new storage that only stores strings:

const storage = createStorage<string>();

await storage.getItem("k"); // => <string | null>

storage.setItem("k", "val"); // Type Check Succeeds
storage.setItem("k", 123); // TypeScript Error: Argument type isn't assignable to the parameter type.

You can, of course, define types for individual keys as well:

await storage.getItem<string>("k"); // => <string | null>

Unstorage Drivers

By default, Unstorage uses in-memory storage. However, you can also mount additional drivers to back your data up to a filesystem, a database, or even a remote server. This feature allows you to combine multiple storage backends into one.

Unstorage provides several built-in drivers that you can use out of the box. Additionally, it's easy to create your custom drivers. Let's look at how we can mount a driver to our storage.

Mounting Drivers

Unstorage's most powerful feature is its ability to mount different storage mechanisms at different mount points in a Unix-like fashion. When you perform operations on a key that starts with a mount point, instead of using the default in-memory storage, the mounted driver for that mount point will be utilized.

Let's see this feature in action by mounting the filesystem driver to store some of our user data. First, we import createStorage and fsDriver from unstorage:

import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";

Now, we create default in-memory storage that we will use around our application:

const storage = createStorage();

Next, say we want to store some of our data in the filesystem. We can mount the filesystem driver to a mount point and use that mount point to store our data:

storage.mount("/fs", fsDriver({ base: "./userData" }));

In the example above, we've mounted the filesystem at "/fs" and specified the base directory as "./userData". Now, storing any data using a key that starts with "/fs" will be stored in the filesystem instead of the in-memory storage.

await storage.setItem("/fs/user:details", {
name: "John Doe",
email: "john@doe.com",
});

You can mount multiple drivers to different mount points and use them to store data. For instance, say we'd also like to mount a Redis driver to store some of our data in Redis:

import redisDriver from "unstorage/drivers/redis";

storage.mount("/redis", redisDriver({ host: "localhost", port: 6379 }));

Now, storing any data using a key that starts with "/redis" will be stored in Redis instead of the in-memory storage.

await storage.setItem("/redis/user:details", {
name: "John Doe",
email: "john@doe.com",
});

Mounting at Root

You can also mount a driver at the root of the storage. This is useful if you want to use a driver for all your data. For example, we're using Unstorage in the browser and want to store all our data in the browser's LocalStorage. We can mount the LocalStorage driver at the root of the storage:

import { createStorage } from "unstorage";
import localStorageDriver from "unstorage/drivers/localstorage";

const storage = createStorage({
driver: localStorageDriver({ base: "<your-app-name>" }),
});

Now, any data that we store will be stored in the browser's LocalStorage.

await storage.setItem("user:details", {
name: "John Doe",
email: "john@doe.com",
});

Working with Namespaces

As I mentioned earlier, it's good practice to use namespaces when storing data in Unstorage. This is because Unstorage stores all data in a flat structure, so if you don't use namespaces, you might have conflicts between keys.

For example, we have used the namespace user:details to store our user data. We can use the same pattern for other data as well, such as user:posts to keep the user's posts, user:comments to store the user's comments, and so on.

During development, writing the namespace every time can be cumbersome, so Unstorage provides a utility function called prefixStorage that can be used to create a new storage instance with a prefix. Let's see how we can use it:

import { prefixStorage } from "unstorage";

const storage = createStorage();
const userStorage = prefixStorage(storage, "user:");

Now, we can use the userStorage just like we used the storage instance earlier, but all keys will be prefixed with "user:".

await userStorage.setItem("details", {
name: "John Doe",
email: "john@doe.com",
});

Wrapping Up

As you can see, Unstorage is a powerful tool that can be used to build complex applications. It provides a simple interface that's easy to learn and use, and it also provides some really powerful features that can be used to build complex applications.

We only covered the basics of Unstorage in this article, but there's a lot more to it. For instance, we didn't cover the watch function that can be used to watch for changes in the storage. We also didn't cover the snapshot function that can be used to create a snapshot of the storage and restore it later. You can learn more about these functions and other features of Unstorage by visiting their API documentation.

In the next article, I'll go deeper into Unstorage and combine a few of its features to build a practical setup for syncing data between a browser and a server. Stay tuned!

Interested in LogSnag?

Get started right now for free!