Scaffold & Config
TL;DR
nuxi init creates a Nuxt project with sensible defaults. nuxt.config.ts is the single source of truth for build-time and runtime configuration. Getting TypeScript strict mode and devtools set up early saves hours of debugging later. Understand the split between build-time config (locked at compile) and runtime config (injectable per environment).
Mental Model
nuxt.config.ts governs two worlds:
- Build-time config β modules, Vite settings, Nitro presets, route rules. These affect the compilation output. Once you run
nuxt build, theyβre baked in. Changing them requires a rebuild. - Runtime config β values available at request time via
useRuntimeConfig(). These can differ per environment (dev vs staging vs prod) without rebuilding. Environment variables override them at deploy time.
This split is fundamental. If you put an API URL in modules, you must rebuild to change it. If you put it in runtimeConfig.public, you can override it with an env var at runtime.
nuxi CLI Walkthrough
npx nuxi init my-app
This scaffolds:
my-app/
βββ app.vue β Entry component (replaces pages/ if you don't need routing)
βββ nuxt.config.ts β Framework config
βββ tsconfig.json β Extends .nuxt/tsconfig.json
βββ package.json β Deps: nuxt, vue, vue-router
βββ .gitignore β Ignores .nuxt/, node_modules/, .output/
βββ README.md
Other useful nuxi commands youβll use daily:
| Command | Purpose |
|---|---|
nuxi dev | Start dev server with HMR |
nuxi build | Production build |
nuxi preview | Preview production build locally |
nuxi prepare | Generate .nuxt/ types without starting server |
nuxi add page <name> | Scaffold a new page |
nuxi add composable <name> | Scaffold a composable |
nuxi add api <name> | Scaffold a server API route |
nuxi module add <name> | Install and register a Nuxt module |
nuxi cleanup | Delete .nuxt/, node_modules/.cache, .output/ |
nuxi analyze | Visualize production bundle |
nuxt.config.ts Anatomy
export default defineNuxtConfig({
// Build-time: which modules to load
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
// Runtime: values accessible via useRuntimeConfig()
runtimeConfig: {
// Private β server only (never sent to client)
apiSecret: '',
database: { host: 'localhost', port: 5432 },
// Public β available on both server and client
public: {
apiBase: 'http://localhost:3000/api',
appName: 'My App',
},
},
// App-level settings
app: {
head: {
title: 'My App',
meta: [{ name: 'description', content: 'Dense Nuxt app' }],
},
},
// Vite config passthrough
vite: {
css: { preprocessorOptions: { scss: { api: 'modern' } } },
},
// Nitro server config
nitro: {
preset: 'node-server', // or 'cloudflare', 'vercel', etc.
compressPublicAssets: true,
},
// Per-route rendering rules
routeRules: {
'/blog/**': { isr: 3600 },
'/admin/**': { ssr: false },
},
// Devtools
devtools: { enabled: true },
// Compatibility date β determines which Nuxt 4 behavior set is active
compatibilityDate: '2025-01-01',
})
Key top-level options:
modulesβ Nuxt modules to load (build-time only)runtimeConfigβ environment-aware config (private + public split)appβ head defaults, global page transitions, root tag settingsviteβ Vite configuration passthroughnitroβ Nitro server configuration (preset, storage, caching)routeRulesβ per-route rendering/caching rulesdevtoolsβ enable/disable Nuxt DevToolscompatibilityDateβ feature flag date for breaking changestypescriptβ TypeScript strictness settingscomponentsβ component auto-import directory configimportsβ composable/util auto-import directory config
TypeScript Strictness
Your tsconfig.json looks minimal:
{
"extends": "./.nuxt/tsconfig.json"
}
All the magic is in .nuxt/tsconfig.json (generated). It includes paths for auto-imports, module resolution, and type references. To enable strict mode:
// nuxt.config.ts
export default defineNuxtConfig({
typescript: {
strict: true, // Enables strict: true in generated tsconfig
typeCheck: true, // Runs vue-tsc during dev and build (slower but catches errors)
},
})
strict: true gives you noImplicitAny, strictNullChecks, strictFunctionTypes β the full suite. Do this from day one. Retrofitting strict mode onto a large codebase is painful.
typeCheck: true runs vue-tsc in parallel during dev. Itβs slower but catches template type errors that your IDE might miss. Enable it in CI at minimum; optionally during dev if your machine handles it.
Devtools Activation
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
})
Press Shift+Alt+D in the browser (or click the Nuxt icon at page bottom) to open DevTools. Tabs youβll use most:
- Overview β active modules, Nuxt version, rendering mode
- Pages β all routes with their middleware and layouts
- Components β component tree with props/state inspection
- Imports β every auto-imported function and its source
- Server Routes β all API endpoints with method and path
- State β reactive state (useState, Pinia stores)
- Payload β SSR payload transferred to client
- Modules β installed modules and their hooks
DevTools are stripped from production builds automatically. No need to disable them for deploy.
Runtime Config and Environment Variables
The runtime config system has a convention for env var overrides:
// nuxt.config.ts
runtimeConfig: {
apiSecret: 'default-dev-secret', // β NUXT_API_SECRET
database: {
host: 'localhost', // β NUXT_DATABASE_HOST
port: 5432, // β NUXT_DATABASE_PORT
},
public: {
apiBase: '/api', // β NUXT_PUBLIC_API_BASE
},
}
The convention: NUXT_ prefix + path in SCREAMING_SNAKE_CASE. Nested objects use underscore separators. This works at runtime β no rebuild needed. Just set the env var and restart the server.
Access in code:
// Server-side (full access)
const config = useRuntimeConfig()
config.apiSecret // β available
config.public.apiBase // β available
// Client-side (public only)
const config = useRuntimeConfig()
config.apiSecret // β undefined (stripped from client bundle)
config.public.apiBase // β available
Walkthrough
Open the 01-scaffold starter:
cd starters/01-scaffold
npm install
npm run dev
- Explore
nuxt.config.ts. Note the devtools and runtimeConfig already configured. - Open DevTools (Shift+Alt+D). Check the Imports tab β see every auto-imported function.
- Test runtimeConfig. Create
server/api/config.get.ts:export default defineEventHandler(() => { const config = useRuntimeConfig() return { secret: config.apiSecret, public: config.public } })Hit
/api/configβ youβll see the private key is accessible server-side. - Test env override. Stop the server. Run
NUXT_API_SECRET=overridden npm run dev. Hit/api/configagain β value changed without rebuild. - Check TypeScript. Open any
.vuefile β auto-imports should resolve without red squiggles. If not, runnpx nuxi prepare.
Gotchas
runtimeConfig.publicis PUBLIC. Itβs serialized into the client bundle. Never put API keys, database credentials, or tokens here. Use the private (top-level) runtimeConfig for secrets, accessible only in server routes.- Changing
nuxt.config.tsrequires dev server restart. Unlike.vuefiles which hot-reload, config changes need a full restart. Nuxt will usually detect this and prompt you. compatibilityDatematters. Itβs not cosmetic. Nuxt 4 uses this date to determine which behavior set is active. Setting it to today opts you into the latest breaking changes. Setting it to an older date preserves legacy behavior. Always use the date when you started the project, then bump it deliberately.- Donβt edit
.nuxt/. Itβs regenerated on everyprepare/dev/build. But DO read.nuxt/tsconfig.jsonwhen debugging type issues β it shows you exactly what paths and types are registered. typeCheck: trueis slow. It spawnsvue-tscin a separate process. During active development, rely on your IDEβs type checking and enabletypeCheckonly in CI or pre-commit hooks.- Module order matters. Modules listed earlier in the array run first. If module B depends on something module A provides, A must come first.
Exercise
- Run
npx nuxi init scratch-appβ scaffold a fresh project. - Configure it:
- Enable
typescript.strict - Enable
devtools - Add
runtimeConfigwithapiSecret: ''(private) andpublic.apiBase: '/api'
- Enable
- Create a server route
server/api/health.get.tsthat returns{ status: 'ok', apiBase: config.public.apiBase }. - Verify the private
apiSecretis NOT accessible from the client. UseuseRuntimeConfig()in a component and check whatβs available. - Override
NUXT_PUBLIC_API_BASE=https://prod.api.comvia env var and confirm the change without rebuilding.