One key-value API across Redis, filesystem, and in-memory drivers
Nitro, the server engine behind Nuxt 3, ships with unstorage, a unified key-value interface that can be backed by Redis, the local filesystem, Cloudflare KV, or plain in-memory storage, without the application code ever needing to know which one is actually in use. Anyone who wants to cache server-side API responses can skip pulling in a separate caching library entirely and simply swap the storage driver per environment through configuration.
Table of Contents
- 1. What unstorage is and why it is already in every Nuxt project
- 2. useStorage() on the server: getting your first storage instance
- 3. Configuring drivers: Redis, filesystem, and in-memory compared
- 4. Practical use case: caching API responses without an external library
- 5. TTL and invalidation: when cache entries should expire
- 6. Keeping namespaces and multiple mounts cleanly separated
- 7. How unstorage differs from a classic database for structured data
- 8. In production: why Redis usually beats filesystem or memory
- 9. Error handling and monitoring around storage access
- 10. Summary
- 11. FAQ
1. What unstorage is and why it is already in every Nuxt project
unstorage is a small library, built by the Nuxt team, that puts one single key-value interface in front of a range of very different storage backends. Whether the data ultimately lives in the server process memory, in a file on disk, in Redis, or in a cloud KV store such as Cloudflare KV makes no difference to the calling code, which always uses the same handful of methods: getItem, setItem, removeItem, and a few related functions for listing keys and metadata. This abstraction is built directly into Nitro and runs automatically in every Nuxt 3 project, even though most projects never notice it until they need it.
The practical payoff shows up once a project runs in more than one environment. Locally, a simple in-memory driver or the filesystem is usually enough, while production typically relies on Redis or a serverless KV store because multiple server instances there need to share the same cache. Because application code only talks to the unstorage API, switching environments only means changing a few lines in nitro.config, not touching a single line of the actual server logic that reads and writes cache entries.
2. useStorage() on the server: getting your first storage instance
Inside any Nitro server route, the useStorage() function is available without an extra import and returns an in-memory instance by default. Calling useStorage('cache') instead reaches into a named mount that was defined earlier in the configuration, which keeps different kinds of data cleanly separated, for example session data versus plain cache entries.
The snippet below shows how to write, read, and remove a value from within a server route. All three operations are asynchronous, because the underlying driver may trigger a network request depending on the backend, for instance with Redis or a cloud KV store, while the in-memory driver would technically respond synchronously; the API is deliberately kept async everywhere so the code stays unchanged when the driver is swapped.
// server/api/hello.get.ts
export default defineEventHandler(async (event) => {
const storage = useStorage('cache')
// Write a value (optionally with TTL support depending on the driver)
await storage.setItem('greeting:last', { text: 'Hello world', ts: Date.now() })
// Read a value, returns null when not present
const cached = await storage.getItem<{ text: string; ts: number }>('greeting:last')
// Existence check without loading the actual value
const exists = await storage.hasItem('greeting:last')
return { cached, exists }
})
3. Configuring drivers: Redis, filesystem, and in-memory compared
Which driver sits behind a named storage mount is configured centrally in the nitro.config block inside nuxt.config.ts. For local development, the filesystem driver is often enough, since it stores each value as an individual file in a configured directory and survives server restarts, while the default driver without any explicit configuration lives purely in memory and is lost on every restart.
In production environments with multiple parallel server instances, as is typical behind a load balancer, a shared store such as Redis becomes almost mandatory, because otherwise every instance would build up its own independent and therefore inconsistent cache. Switching drivers only touches configuration, the application code that calls useStorage() stays identical in every case, which turns a later move from filesystem to Redis into a pure configuration change.
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
storage: {
// Named mount 'cache' points to Redis in production
cache: {
driver: 'redis',
host: process.env.REDIS_HOST,
port: 6379,
password: process.env.REDIS_PASSWORD,
},
},
devStorage: {
// In development, use a simple filesystem driver instead
cache: {
driver: 'fs',
base: './.data/cache',
},
},
},
})
4. Practical use case: caching API responses without an external library
One of the most common uses for unstorage is caching responses from external APIs that change rarely but would otherwise be requested again on every single page render if no cache existed. Instead of adding a dedicated caching library for this, it is enough to check the storage for an already valid entry before the actual fetch call, and only hit the external API on a cache miss.
This pattern not only saves latency for your own users, it also protects the rate limit of the external API, which can matter a lot with third-party services that enforce a strict quota, sometimes making the difference between a stable integration and one that gets blocked regularly. The implementation stays deliberately simple, without pulling in packages such as node-cache or lru-cache, because unstorage already provides everything needed.
5. TTL and invalidation: when cache entries should expire
unstorage itself has no built-in TTL concept at the API level that behaves identically across every driver, so in practice a timestamp stored alongside the value has proven to be the simplest approach. On read, the code checks not only whether a value exists but also whether the time elapsed since it was written has already exceeded the allowed validity window, treating the entry as expired and reloading it if so.
For explicit invalidation, for example after editorial content changes in a CMS, a webhook that calls removeItem or clears a specific prefix on arrival works well. It helps to use consistent key naming with a clear namespace, such as products:list or products:id:123, so that targeted deletions are possible without accidentally wiping unrelated cache entries in the process.
6. Keeping namespaces and multiple mounts cleanly separated
In larger projects it pays off to set up several named storage mounts for different purposes instead of mixing everything into a single namespace. One mount for short-lived API cache, another for longer-lived configuration data, and a third for session-like data can each be configured independently, even with different drivers, whenever the requirements around persistence and speed diverge.
Even within a single mount, a consistent key prefix convention makes it much easier to later delete or list a specific subset of entries, for example through storage.getKeys('products:'), which returns only keys starting with that prefix. This structure pays off especially once a project grows and several teams add cache entries independently, without accidentally overwriting each other's keys.
7. How unstorage differs from a classic database for structured data
unstorage is deliberately not a replacement for a relational or document database. It has no schema, no relationships between entries, and no complex query capabilities beyond direct key access or simple prefix listing. Anyone who needs to filter, sort, or join data across multiple tables is far better served by a real database such as PostgreSQL or MongoDB.
The strength of unstorage instead lies in simple, very fast storage and retrieval of individual values through a known key, which is exactly the access pattern cache data typically follows. Structured content with relationships, validation rules, or transactional guarantees still belongs in a dedicated database, while unstorage plays the role of a fast layer in front of it that absorbs repeated, expensive lookups.
8. In production: why Redis usually beats filesystem or memory
The in-memory driver is convenient for tests and local development because it needs no external dependency, but it loses every cache entry on process restart and only works within a single server instance. In production environments with several instances running in parallel, as is common with horizontal scaling, that means each instance builds up its own independent and therefore inconsistent cache.
The filesystem driver solves the persistence problem but still does not automatically share state across multiple instances, unless every instance mounts a shared network filesystem, which adds its own complexity and potential latency. Redis solves both problems at once by being persistent, very fast, and equally reachable from every instance, which is why it has become the de facto standard for shared cache in production Nuxt deployments running behind more than one server.
9. Error handling and monitoring around storage access
Because every storage access against an external driver like Redis can trigger a network request, calling code should never assume that getItem or setItem always succeed. A brief connection drop to the Redis server should ideally not crash the entire request; instead it should be caught in a try-catch block so the application can fall back to loading data directly from the source when the cache is temporarily unavailable.
For production operation, it is also worth counting cache hits and misses and surfacing them through a monitoring system, in order to notice early when the hit rate drops unusually far, for example because a TTL was set too short or a Redis cluster is evicting entries early under memory pressure. That observability is the difference between a cache that silently loses its effect and one whose behavior can actually be tracked over time.
| Driver | Persistence | Shared across instances | Typical use |
|---|---|---|---|
| memory (default) | No, lost on restart | No | Local development, tests |
| fs (filesystem) | Yes, survives restarts | Only with a shared network path | Single-server deployments |
| redis | Yes, configurably persistent | Yes, natively | Production with multiple instances |
| cloudflare-kv | Yes, managed | Yes, globally distributed | Edge deployments on Cloudflare |
| http | Depends on the target server | Yes, via shared endpoint | Integrating existing cache services |
Mironsoft
Vue architecture, Composition API, and Nuxt performance
Vue applications that don't get more complicated with every feature?
We review existing Vue and Nuxt projects for unstructured composables, unnecessary reactivity, and bloated bundles, then build an architecture that absorbs new features without making the codebase harder to follow.
Architecture Review
Checking composables, state management, and component structure for maintainability.
Performance Audit
Systematically optimizing reactivity overhead, bundle size, and Nuxt rendering strategy.
Nuxt Integration
Building robust, type-safe SSR/SSG setup and API integration.
10. Summary
Nitro Storage and unstorage in Nuxt: the essentials at a glance
What unstorage is
A unified key-value API inside Nitro covering Redis, filesystem, in-memory, and more backends.
Core call
useStorage('name') returns a named storage instance with getItem, setItem, and removeItem.
Typical use
Server-side caching of API responses without pulling in an external caching library.
Limitation
No replacement for a relational database, no schema, no complex queries.