KV Storage

Nitro provides a built-in storage layer that can abstract the filesystem, a database, or any other data source.

Nitro has built-in integration with unstorage to provide a runtime-agnostic persistence layer: your code always calls the same key-value API, and the backing driver (memory, filesystem, Redis, ...) is swapped through configuration.

#Usage

To use the storage layer, you can use the useKV() utility to access the storage instance.

import { useKV } from "nitro/kv";

// Default storage (in-memory)
await useKV().setItem("test:foo", { hello: "world" });
const value = await useKV().getItem("test:foo");

// You can specify a base prefix with useKV(base)
const testStorage = useKV("test");
await testStorage.setItem("foo", { hello: "world" });
await testStorage.getItem("foo"); // { hello: "world" }

// You can use generics to type the return value
await useKV<{ hello: string }>("test").getItem("foo");
await useKV("test").getItem<{ hello: string }>("foo");
Read more in unstorage.unjs.io.

Note

useKV was previously named useStorage and exported from nitro/storage, and the kv config option was previously named storage. The old names still work as deprecated aliases. The deprecated devStorage option is replaced by kv inside $development (and $prerender).

#Available methods

The storage instance returned by useKV() provides the following methods:

MethodDescription
getItem(key)Get the value of a key. Returns null if the key does not exist.
getItems(items)Get multiple items at once. Accepts an array of keys or { key, options } objects.
getItemRaw(key)Get the raw value of a key without parsing. Useful for binary data.
setItem(key, value)Set the value of a key.
setItems(items)Set multiple items at once. Accepts an array of { key, value } objects.
setItemRaw(key, value)Set the raw value of a key without serialization.
hasItem(key)Check if a key exists. Returns a boolean.
removeItem(key)Remove a key from storage.
getKeys(base?)Get all keys, optionally filtered by a base prefix.
clear(base?)Clear all keys, optionally filtered by a base prefix.
getMeta(key)Get metadata for a key (e.g., mtime, atime, ttl).
setMeta(key, meta)Set metadata for a key.
removeMeta(key)Remove metadata for a key.
mount(base, driver)Dynamically mount a storage driver at a base path.
unmount(base)Unmount a storage driver from a base path.
watch(callback)Watch for changes. Callback receives (event, key) where event is "update" or "remove".
unwatch()Stop watching for changes.

Shorthand aliases are also available: get, set, has, del, remove, keys.

import { useKV } from "nitro/kv";

// Get all keys under a prefix
const keys = await useKV("test").getKeys();

// Check if a key exists
const exists = await useKV().hasItem("test:foo");

// Remove a key
await useKV().removeItem("test:foo");

// Get raw binary data
const raw = await useKV().getItemRaw("assets:server:image.png");

// Get metadata (type, etag, mtime, etc.)
const meta = await useKV("assets:server").getMeta("file.txt");

#Configuration

You can mount one or multiple custom storage drivers using the kv option.

The key is the mount point name, and the value is the driver name and configuration.

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  kv: {
    redis: {
      driver: "redis",
      /* redis connector options */
    }
  }
})

Then, you can use the redis storage using the useKV("redis") function.

Read more in unstorage.unjs.io/.

Important

Mount options are serialized into the build output, so they must be JSON-serializable. Use runtime configuration if a mount needs a non-serializable option.

#Driver dependencies

Some drivers rely on a third-party library (for example, redis requires ioredis).

Nitro detects the libraries required by the mounted drivers and prompts to install the missing ones (installed automatically in CI). Installed libraries are then explicitly passed to the driver via its lib option, so that bundlers can statically resolve them. lib options are therefore not configurable in kv mounts (set them to null to opt out of the injected import).

#Development storage

You can use the $development config key to override kv mounts during development.

This is useful when your production driver is not available in development (e.g., a managed Redis instance).

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  kv: {
    db: {
      driver: "redis",
      host: "prod.example.com",
    }
  },
  $development: {
    kv: {
      db: {
        driver: "fs",
        base: "./.data/db"
      }
    }
  }
})

When running in development mode, $development.kv mounts are merged on top of kv mounts, allowing you to use a local filesystem driver or an in-memory driver while developing. A mount with a different driver replaces the whole mount (host is not passed to the fs driver above). A mount with the same driver has its options deep merged.

$development only applies to the development server. When prerendering a production build, $production and then $prerender overrides apply instead. Use $prerender to override mounts during prerendering only:

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  kv: {
    db: { driver: "redis", host: "prod.example.com" }
  },
  $development: {
    kv: { db: { driver: "fs", base: "./.data/db" } }
  },
  $prerender: {
    kv: { db: { driver: "fs", base: "./.data/db" } }
  }
})

Tip

A common setup is a hosted driver (e.g. redis) in kv for production and an fs driver in $development.kv, so development data is easy to inspect locally in .data/ and does not require external services.

#Built-in mount points

Nitro automatically mounts the following storage paths:

#/assets

Server assets are mounted at the /assets base path (see Server assets). In production, this mount is read-only and only supports getKeys(), hasItem(), getItem(), getItemRaw(), and getMeta().

import { useKV } from "nitro/kv";

// Access server assets via the /assets mount
const content = await useKV("assets:server").getItem("my-file.txt");

#Default (in-memory)

The root storage (without a base path) uses an in-memory driver by default. Data stored here is not persisted across restarts.

import { useKV } from "nitro/kv";

// In-memory by default, not persisted
await useKV().setItem("counter", 1);

To persist data, mount a driver with a persistent backend (e.g., fs, redis, etc.) using the kv configuration option.

Note

The cache layer also stores its entries in this root storage (under the cache: prefix) unless you configure a dedicated cache mount point, so cached data is not persisted across restarts by default either.

#Server assets

Nitro allows you to bundle files from an assets/ directory. These files are accessible at runtime via the assets:server storage mount.

The directory is resolved relative to serverDir when it is set, and to the project root otherwise:

my-project/
  server/
    assets/
      data.json
      templates/
        welcome.html
    routes/
      index.ts
server/routes/index.ts
import { defineHandler } from "nitro";
import { useKV } from "nitro/kv";

export default defineHandler(async () => {
  const serverAssets = useKV("assets:server");

  const keys = await serverAssets.getKeys();
  const data = await serverAssets.getItem("data.json");
  const template = await serverAssets.getItem("templates/welcome.html");

  return { keys, data, template };
});

#Custom asset directories

You can register additional asset directories using the serverAssets config option. Each dir is resolved relative to rootDir (not serverDir):

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  serverAssets: [
    {
      baseName: "templates",
      dir: "./templates",
    }
  ]
})

Custom asset directories are accessible under assets:<baseName>:

import { useKV } from "nitro/kv";

const templates = useKV("assets:templates");
const keys = await templates.getKeys();
const html = await templates.getItem("email.html");

#Asset metadata

Server assets include metadata such as content type, ETag, and modification time:

import { useKV } from "nitro/kv";

const serverAssets = useKV("assets:server");

const meta = await serverAssets.getMeta("image.png");
// { type: "image/png", etag: "\"...\"", mtime: "2024-01-01T00:00:00.000Z" }

// Useful for setting response headers
const raw = await serverAssets.getItemRaw("image.png");

Note

In development (and while prerendering), server assets are read directly from the filesystem with the fs driver, so getMeta() returns filesystem stats (mtime, size, ...) instead of type and etag. In production, assets are bundled into the build output with their metadata pre-computed at build time.

Read more in Docs > Assets.

#Runtime configuration

In scenarios where the mount point configuration is not known until runtime, Nitro can dynamically add mount points during startup using plugins.

plugins/storage.ts
import { useKV } from "nitro/kv";
import { definePlugin } from "nitro";
import redisDriver from "unstorage/drivers/redis";

export default definePlugin(() => {
  const storage = useKV()

  // Dynamically pass in credentials from runtime configuration, or other sources
  const driver = redisDriver({
    base: "redis",
    host: process.env.REDIS_HOST,
    port: Number(process.env.REDIS_PORT),
    // Pass the driver library explicitly so that it is bundled
    lib: () => import("ioredis"),
    /* other redis connector options */
  })

  // Mount driver
  storage.mount("redis", driver)
})

Important

Drivers mounted this way are outside of Nitro's driver dependency detection: install the third-party library yourself (ioredis in the example above) and pass it via the driver's lib option. Without it, the driver falls back to a dynamic import("ioredis") that bundlers cannot statically resolve, and the mount fails at runtime with a Cannot import ioredis error.

Nitro  builds full-stack servers that deploy anywhere.