Storing data on the server
A list in server memory disappears on every restart. To keep posts, they have to be stored somewhere. Nuxt has a built-in Nitro storage for this, useStorage. It is a key-value store: under a string key lies any value that can be written as JSON. The storage can point to different places, such as a folder on disk, Redis or the cloud; the course uses a folder on disk.
Setup
Describe the storage in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
nitro: {
storage: {
blog: { driver: 'fs', base: './.data/blog' }
}
}
})The storage is named blog, the fs driver writes data to files, and base is their folder. Add the line .data to .gitignore so the data does not end up in git, and restart npm run dev.
Reading and writing
// server/api/visits.get.ts
export default defineEventHandler(async () => {
const storage = useStorage('blog')
const visits = ((await storage.getItem<number>('visits')) ?? 0) + 1
await storage.setItem('visits', visits)
return { visits }
})getItem reads the value under a key (or null if there is none), and setItem writes it. Refresh /api/visits: the number grows and does not reset after a server restart, because it lives in the file .data/blog/visits.
Posts on disk
Replace server/utils/posts.ts:
// server/utils/posts.ts
export interface Post {
id: number
title: string
text: string
}
export interface PostComment {
author: string
text: string
}
export async function getPosts() {
return (await useStorage('blog').getItem<Post[]>('posts')) ?? []
}
export async function savePosts(posts: Post[]) {
await useStorage('blog').setItem('posts', posts)
}Now the handlers get posts through these functions:
// server/api/posts.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody<{ title?: string; text?: string }>(event)
if (!body.title || !body.text) {
throw createError({ statusCode: 400, message: 'Fill in the title and the text' })
}
const posts = await getPosts()
const post = {
id: Math.max(0, ...posts.map((p) => p.id)) + 1,
title: body.title,
text: body.text
}
posts.push(post)
await savePosts(posts)
setResponseStatus(event, 201)
return post
})In server/api/posts.get.ts and server/api/posts/[id].get.ts, replace the use of the posts array with the line const posts = await getPosts() and make the handlers async. Added posts now survive a restart.
Comments
Keep the comments of each post under their own key. The PostComment type is declared above in server/utils/posts.ts:
// server/api/posts/[id]/comments.get.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
return (await useStorage('blog').getItem<PostComment[]>(`comments:${id}`)) ?? []
})// server/api/posts/[id]/comments.post.ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const body = await readBody<{ author?: string; text?: string }>(event)
if (!body.author || !body.text) {
throw createError({ statusCode: 400, message: 'Enter your name and the text' })
}
const storage = useStorage('blog')
const comments = (await storage.getItem<PostComment[]>(`comments:${id}`)) ?? []
comments.push({ author: body.author, text: body.text })
await storage.setItem(`comments:${id}`, comments)
setResponseStatus(event, 201)
return comments
})The fs driver turns a key with a colon, comments:1, into the file .data/blog/comments/1. The comments of the first post are at /api/posts/1/comments.
When you need a real database
A key-value store is fine for a small blog. When you need searching by fields, relations and many records, connect a database such as SQLite, PostgreSQL or MySQL through its Node.js library. You use it from handlers in server/ in the same way.
Common mistake
Calling useStorage('blog') without describing the storage in nitro.storage. There is no error, but Nitro keeps that data in memory, and it disappears after a restart. If data vanishes, check the setting and restart the server after editing nuxt.config.ts.