writing-skills
writing-skills
Use when creating new skills, editing existing skills, or verifying skills work before deployment
nitropackBuilds portable server applications with Nitro (nitropack), the server engine behind Nuxt, Analog and SolidStart: file-based routes, middleware, a storage layer, response caching, scheduled tasks and one build that targets Node.js, Bun, Deno, Cloudflare, Vercel, Netlify and AWS. Use when a user asks to create a Nitro server, add API routes or tasks, configu…
Independent GitHub snapshot · Automated classification · Original documentation governs
Nitro turns a folder of handlers into a server that builds to a self-contained .output/ directory. The same code runs on Node.js, Bun, Deno, Cloudflare Workers, Vercel, Netlify, AWS and others; the build target is chosen by a preset. Nuxt uses Nitro for everything under its server/ directory, so this skill applies to Nuxt server routes too.
Two lines exist side by side (checked October 2026):
nitropack (2.13.x, latest). H3 version 1 underneath. Auto-imports of defineEventHandler, useStorage and friends. This is what this skill's examples use, and what Nuxt 3/4 ship.nitro (3.0 beta). H3 version 2, no auto-imports, directory scanning is opt-in, useStorage is renamed useKV. See "Moving to Nitro 3" below before starting a new project on it.npx giget@latest nitro orders-api --install # starter template (Nitro 2, nitropack)
cd orders-api
npm run dev # nitro dev, http://localhost:3000
npm run build # writes .output/
npm run preview # node .output/server/index.mjs (PORT or NITRO_PORT sets the port)
The starter's nitro.config.ts sets srcDir: "server" and imports: false. Two consequences that surprise people:
nitropack install and no srcDir, Nitro scans routes/, api/, middleware/, tasks/ at the project root, not under server/. A server/routes/users.post.ts then returns 404 with no warning. Set srcDir: "server" (Nuxt already does).imports: false there are no auto-imports: import { defineEventHandler, getRouterParam } from "h3" and import { useStorage } from "nitropack/runtime". The examples below omit imports, which works only when auto-imports are on (the default when you write your own config, and always in Nuxt).Files under server/routes/ map to URLs; server/api/ adds the /api prefix. A .get.ts/.post.ts suffix pins the method, [id] is a param, [...slug] a catch-all, and .dev.ts/.prod.ts limits a file to one build mode. One handler per file.
// server/routes/users/[id].get.ts -> GET /users/42
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const user = await useStorage("db").getItem(`users:${id}`);
if (!user) throw createError({ statusCode: 404, message: "User not found" });
return user;
});
// server/routes/users.post.ts -> POST /users
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (!body?.name || !body?.email) {
throw createError({ statusCode: 400, message: "name and email are required" });
}
const id = crypto.randomUUID();
await useStorage("db").setItem(`users:${id}`, { id, ...body, createdAt: Date.now() });
setResponseStatus(event, 201);
return { id, ...body };
});
Files in server/middleware/ run before every route. A middleware may change the request or event.context; it must not return a value, because a returned value becomes the response and stops the request. Files in server/utils/ are auto-imported (Nitro 2 with imports on).
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
if (getRequestURL(event).pathname.startsWith("/api/public")) return;
const token = getHeader(event, "authorization")?.replace("Bearer ", "");
if (!token) throw createError({ statusCode: 401, message: "Unauthorized" });
event.context.user = await verifyToken(token); // verifyToken lives in server/utils/
});
useStorage(base) is a key-value layer over drivers. The first argument is a mount point; keys use : separators. Without configuration the default mount is in memory and is lost on restart.
// nitro.config.ts
export default defineNitroConfig({
srcDir: "server",
compatibilityDate: "2025-01-30",
storage: {
db: { driver: "fs", base: "./.data/db" }, // local files
sessions: { driver: "redis", url: process.env.REDIS_URL }, // see unstorage drivers
},
devStorage: { sessions: { driver: "fs", base: "./.data/sessions" } }, // dev only
});
Mount points root, src, build and cache exist in development; do not reuse the name cache for your own data, because Nitro's response cache lives there. For credentials that are not known at build time, mount the driver in a defineNitroPlugin using useRuntimeConfig().
// server/routes/stats.get.ts — cached 1 hour, stale value served while refreshing
export default defineCachedEventHandler(() => computeStats(), { maxAge: 60 * 60 });
// server/utils/github.ts — cache any function; results must be JSON-serializable
export const cachedStars = defineCachedFunction(
async (repo: string) => (await $fetch<any>(`https://api.github.com/repos/${repo}`)).stargazers_count,
{ maxAge: 3600, name: "ghStars", getKey: (repo: string) => repo },
);
Request headers are dropped when building the cache key; list the ones that matter in varies: ["accept-language"]. On edge workers pass event as the first argument of cached functions so waitUntil can finish the refresh. For rules by path, use routeRules: { "/blog/**": { swr: 600 } } in the config.
Tasks need experimental: { tasks: true }. A file server/tasks/db/migrate.ts is the task db:migrate. Schedules use cron patterns and run with the croner engine on node-server, bun and deno-server; on cloudflare_module they map to Cron Triggers, so repeat the same patterns in wrangler.toml.
// nitro.config.ts (add to the config above)
experimental: { tasks: true },
scheduledTasks: { "*/5 * * * *": ["cleanup"] },
// server/tasks/cleanup.ts
export default defineTask({
meta: { name: "cleanup", description: "Delete expired sessions" },
async run() {
const keys = await useStorage("sessions").getKeys();
return { result: `${keys.length} sessions checked` };
},
});
While nitro dev is running: nitro task list and nitro task run cleanup --payload "{}", or GET /_nitro/tasks. A task name defined in scheduledTasks but missing as a file prints "Scheduled task ... is not defined!" at build time.
WebSockets are experimental: set experimental: { websocket: true }, then export defineWebSocketHandler({ open, message, close, error }) from server/routes/_ws.ts (any route file works). For one-way streams use createEventStream(event) in an ordinary handler and return eventStream.send().
The default preset is node-server. Zero-config detection exists for Vercel, Netlify, Cloudflare, Azure, AWS Amplify, Firebase App Hosting, Stormkit and Zeabur when building in their CI. Otherwise choose a preset explicitly:
nitro build --preset cloudflare_module # also: NITRO_PRESET=... or preset: in config
node .output/server/index.mjs # node-server output
Preset names use underscores in the docs (cloudflare_pages, cloudflare_module, aws_lambda, deno_deploy). cloudflare_module is the recommended Cloudflare preset; cloudflare_pages is for Pages-specific needs. Set compatibilityDate so provider behavior does not change under you.
nitropack with nitro in package.json; use Node.js 20 or newer.import { defineConfig } from "nitro" and serverDir: "./server" (scanning is off by default; srcDir is deprecated).defineHandler, definePlugin, defineErrorHandler from "nitro"; useKV from "nitro/kv" (was useStorage; config key storage becomes kv); defineCachedFunction/defineCachedHandler from "nitro/cache"; defineTask/runTask from "nitro/task"; useRuntimeConfig from "nitro/runtime-config"; H3 utilities from "nitro/h3".defineHandler replaces defineEventHandler; return the body or throw new HTTPError(...) (createError is gone); read bodies with await event.req.json() instead of readBody; headers are event.req.headers.get(...). app.config.ts support is removed.nitro() from "nitro/vite" to vite.config.ts and use vite dev / vite build; new projects start with npx create-nitro-app. nitro build still works and runs the Vite build.Request: "Set up a small Nitro API that stores orders on disk and exposes GET /orders/:id and POST /orders."
npx giget@latest nitro orders-api --install && cd orders-api
mkdir -p server/routes/orders
Add storage: { orders: { driver: "fs", base: "./.data/orders" } } to nitro.config.ts, put the two handlers from "Routes" in server/routes/orders/[id].get.ts and server/routes/orders.post.ts (storage name orders), then:
npm run build && PORT=3111 node .output/server/index.mjs &
curl -s -X POST localhost:3111/orders -H 'content-type: application/json' -d '{"name":"Alice","email":"alice@shop.test"}'
# {"id":"428279da-52bb-4706-a5bb-c4ccb8b0584e","name":"Alice","email":"alice@shop.test"}
curl -s localhost:3111/orders/428279da-52bb-4706-a5bb-c4ccb8b0584e
The file .data/orders/<id> holds the JSON. A request for an unknown id returns {"statusCode":404,"message":"User not found",...}. If every route answers "Cannot find any route", the config is missing srcDir: "server".
Request: "The /reports/daily endpoint takes 4 seconds; build it every night at 02:00 and serve the stored copy."
// server/tasks/reports/build.ts
export default defineTask({
meta: { name: "reports:build", description: "Build the daily sales report" },
async run() {
const report = await buildDailyReport(); // slow query in server/utils/
await useStorage("db").setItem("reports:daily", report);
return { result: `report with ${report.rows.length} rows stored` };
},
});
// server/routes/reports/daily.get.ts
export default defineEventHandler(async () => {
return (await useStorage("db").getItem("reports:daily")) ?? { rows: [], note: "not built yet" };
});
In nitro.config.ts: experimental: { tasks: true }, scheduledTasks: { "0 2 * * *": ["reports:build"] }. While nitro dev runs, nitro task run reports:build prints the task result and curl localhost:3000/reports/daily returns the stored report in milliseconds. For a response that may simply be a little stale, defineCachedEventHandler(handler, { maxAge: 86400 }) is the smaller change.
nitropack@^2 for production Nuxt 3/4 and existing projects; Nitro 3 is a beta and its APIs still change.readBody returns whatever the client sent).runTask or /_nitro/tasks from a public route without authentication.nitro.config.ts; read them from environment variables or runtimeConfig (NITRO_ prefixed variables override it at runtime).