15.2 Environment Setup — nuxi init, Directory Structure, nuxt.config.ts
Creating a Project
Nuxt 3 projects are created using the official CLI tool, nuxi.
Basic Project Creation
# Create a project with the latest nuxi
npx nuxi@latest init my-nuxt-app
# Navigate to the project directory
cd my-nuxt-app
# Install dependencies
npm install
# Start the development server
npm run dev
Once the dev server is running, you can view your app at http://localhost:3000.
Selecting Options During Setup
The nuxi initialization process asks a few questions to configure your project.
? Which package manager would you like to use?
❯ npm
pnpm
yarn
bun
? Initialize git repository?
❯ Yes
No
Creating with Additional Modules
# With TypeScript + ESLint + Tailwind CSS
npx nuxi@latest init my-app --template github:nuxt/starter#v3
# Or with the Nuxt UI starter
npx nuxi@latest init my-app -t ui
Directory Structure
Understanding the full directory structure of a Nuxt 3 project is important. Each directory has a special role.
Full Structure Overview
my-nuxt-app/
├── .nuxt/ # Build cache (auto-generated, git ignored)
├── .output/ # Production build output
├── assets/ # Static files processed by the build tool
│ ├── css/
│ │ └── main.css
│ └── images/
├── components/ # Vue components (auto-imported)
│ ├── AppHeader.vue
│ ├── AppFooter.vue
│ └── ui/
│ ├── Button.vue
│ └── Card.vue
├── composables/ # Composable functions (auto-imported)
│ ├── useAuth.ts
│ └── useApi.ts
├── layouts/ # Layout components
│ ├── default.vue
│ └── admin.vue
├── middleware/ # Route middleware
│ ├── auth.ts
│ └── logger.ts
├── pages/ # File-based routing
│ ├── index.vue
│ ├── about.vue
│ └── users/
│ ├── index.vue
│ └── [id].vue
├── plugins/ # Nuxt plugins
│ ├── analytics.client.ts # Client-only
│ └── auth.server.ts # Server-only
├── public/ # Static files served as-is
│ ├── favicon.ico
│ └── robots.txt
├── server/ # Server-side code
│ ├── api/ # API routes
│ │ └── users/
│ │ ├── index.get.ts
│ │ └── [id].get.ts
│ ├── middleware/ # Server middleware
│ │ └── logger.ts
│ └── utils/ # Server utilities (auto-imported)
│ └── db.ts
├── utils/ # Shared client/server utilities (auto-imported)
│ └── format.ts
├── app.vue # App root component
├── nuxt.config.ts # Nuxt configuration
├── tsconfig.json # TypeScript configuration
└── package.json
Key Directory Descriptions
pages/ — File-Based Routing
pages/
├── index.vue → /
├── about.vue → /about
├── blog/
│ ├── index.vue → /blog
│ └── [slug].vue → /blog/:slug
└── admin/
├── index.vue → /admin
└── users/
├── index.vue → /admin/users
└── [id]/
├── index.vue → /admin/users/:id
└── edit.vue → /admin/users/:id/edit
components/ — Auto-Imported Components
components/
├── Button.vue → <Button />
├── TheHeader.vue → <TheHeader /> (convention for global singleton components)
└── form/
├── Input.vue → <FormInput />
└── Select.vue → <FormSelect />
Folder structure becomes the component name prefix.
server/api/ — API Routes
File naming convention: [path].[method].ts
server/api/
├── users/
│ ├── index.get.ts → GET /api/users
│ ├── index.post.ts → POST /api/users
│ └── [id].get.ts → GET /api/users/:id
└── auth/
├── login.post.ts → POST /api/auth/login
└── logout.post.ts → POST /api/auth/logout
assets/ vs public/
assets/ | public/ | |
|---|---|---|
| Processing | Bundled by Vite/Webpack | Copied as-is |
| URL | Includes hash (/img.abc123.png) | As-is (/img.png) |
| Import | ~/assets/img.png | /img.png |
| Use cases | CSS, images, fonts | favicon, robots.txt |
Complete Guide to nuxt.config.ts
nuxt.config.ts handles all configuration for your Nuxt application.
Basic Structure
// nuxt.config.ts
export default defineNuxtConfig({
// Enable dev tools (automatically works in dev mode only)
devtools: { enabled: true },
})
Complete Configuration Example
// nuxt.config.ts
export default defineNuxtConfig({
// ─── Dev Tools ───
devtools: { enabled: true },
// ─── TypeScript ───
typescript: {
strict: true, // Strict type checking
typeCheck: true, // Type-check on build
},
// ─── Modules ───
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxtjs/i18n',
'@vueuse/nuxt',
'@nuxt/image',
],
// ─── CSS ───
css: ['~/assets/css/main.css'],
// ─── Runtime Config (Environment Variables) ───
runtimeConfig: {
// Server-only (inaccessible on the client)
dbPassword: process.env.DB_PASSWORD,
jwtSecret: process.env.JWT_SECRET,
// public: accessible on both client and server
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:3000',
appName: 'My Nuxt App',
}
},
// ─── App Configuration ───
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
title: 'My Nuxt App',
meta: [
{ name: 'description', content: 'A full-stack app built with Nuxt 3.' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
],
},
// Page transition animations
pageTransition: { name: 'page', mode: 'out-in' },
layoutTransition: { name: 'layout', mode: 'out-in' },
},
// ─── Router Configuration ───
router: {
options: {
// Hash-based routing (useful for SPA deployment)
// hashMode: true,
}
},
// ─── Build Optimization ───
build: {
// Transpile list
transpile: ['@vue/apollo-composable'],
},
// ─── Vite Configuration ───
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "~/assets/scss/variables" as *;',
},
},
},
// Dev server proxy
server: {
proxy: {
'/external-api': {
target: 'https://api.external.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/external-api/, ''),
}
}
}
},
// ─── Nitro (Server Engine) ───
nitro: {
preset: 'node-server',
// Server storage
storage: {
redis: {
driver: 'redis',
host: process.env.REDIS_HOST || 'localhost',
port: 6379,
}
},
// Route rules
routeRules: {
'/': { prerender: true }, // Pre-render statically
'/blog/**': { swr: 3600 }, // 1-hour cache (ISR)
'/api/**': { cors: true }, // Enable CORS for APIs
'/admin/**': { ssr: false }, // Switch to CSR
}
},
// ─── Experimental Features ───
experimental: {
payloadExtraction: false, // Optimize static deployment
typedPages: true, // Type-safe routing
},
})
Using routeRules
routeRules is a powerful Nuxt 3 feature that lets you configure a different rendering strategy per page.
nitro: {
routeRules: {
// Homepage: pre-rendered (static)
'/': { prerender: true },
// Blog listing: refreshed every hour (SWR = Stale While Revalidate)
'/blog': { swr: 3600 },
// Blog posts: cached 24 hours, then refreshed
'/blog/**': { swr: 86400 },
// Product pages: CDN cache for 1 day
'/products/**': { cache: { maxAge: 60 * 60 * 24 } },
// Admin pages: SSR disabled (CSR)
'/admin/**': { ssr: false },
// API: enable CORS, no caching
'/api/**': { cors: true, headers: { 'cache-control': 'no-cache' } },
// Legacy URL redirect
'/old-page': { redirect: '/new-page' },
}
}
Environment Variables and Runtime Config
Setting Up .env
# .env
# Server-only variables
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=my-secret-key-here
REDIS_URL=redis://localhost:6379
# Public variables (NUXT_PUBLIC_ prefix)
NUXT_PUBLIC_API_BASE_URL=https://api.example.com
NUXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
Using runtimeConfig
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
// Server-only
databaseUrl: '', // Auto-overridden by process.env.NUXT_DATABASE_URL
jwtSecret: '', // Auto-overridden by process.env.NUXT_JWT_SECRET
public: {
// Client + server
apiBase: '', // Overridden by process.env.NUXT_PUBLIC_API_BASE
}
}
})
// server/api/users/index.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// Private variables are accessible on the server
console.log(config.jwtSecret) // Server-only
console.log(config.public.apiBase) // Public variable
})
<!-- pages/index.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()
// Only public variables are accessible on the client
console.log(config.public.apiBase) // OK
// console.log(config.jwtSecret) // undefined (inaccessible for security)
</script>
Practical Example: E-Commerce Project Setup
A complete configuration example for a real-world e-commerce project.
Project Initialization
# Create the project
npx nuxi@latest init my-shop
cd my-shop
# Install required modules
npm install @pinia/nuxt @pinia/nuxt @nuxtjs/tailwindcss
npm install @vueuse/nuxt @nuxt/image
npm install -D @nuxtjs/i18n
# Dev dependencies
npm install -D @nuxt/devtools
Complete nuxt.config.ts
// nuxt.config.ts
export default defineNuxtConfig({
devtools: { enabled: true },
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@vueuse/nuxt',
'@nuxt/image',
],
css: [
'~/assets/css/main.css',
],
runtimeConfig: {
// Server-only environment variables
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY || '',
},
db: {
url: process.env.DATABASE_URL || '',
},
jwt: {
secret: process.env.JWT_SECRET || 'dev-secret',
expiresIn: '7d',
},
public: {
appName: 'My Shop',
stripe: {
publishableKey: process.env.NUXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
},
features: {
reviews: true,
wishlist: true,
chat: false,
}
}
},
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
htmlAttrs: { lang: 'en' },
titleTemplate: '%s | My Shop',
meta: [
{ name: 'theme-color', content: '#3b82f6' },
{ property: 'og:type', content: 'website' },
],
},
pageTransition: { name: 'fade', mode: 'out-in' },
},
nitro: {
routeRules: {
// Home and policy pages are statically generated
'/': { prerender: true },
'/about': { prerender: true },
'/privacy': { prerender: true },
// Product listing: refreshed every 10 minutes
'/products': { swr: 600 },
'/products/**': { swr: 3600 },
// Cart/checkout: SSR only (no cache)
'/cart': { ssr: true, headers: { 'cache-control': 'no-store' } },
'/checkout': { ssr: true, headers: { 'cache-control': 'no-store' } },
// Account pages: CSR (personalized data)
'/account/**': { ssr: false },
'/orders/**': { ssr: false },
// API
'/api/public/**': { cors: true, cache: { maxAge: 60 } },
'/api/user/**': { headers: { 'cache-control': 'no-store' } },
}
},
// Tailwind CSS configuration
tailwindcss: {
configPath: '~/tailwind.config.ts',
exposeConfig: true,
},
// Image optimization
image: {
quality: 80,
formats: ['avif', 'webp'],
domains: ['cdn.example.com'],
presets: {
thumbnail: {
modifiers: { width: 300, height: 300, fit: 'cover' }
},
product: {
modifiers: { width: 600, height: 600, fit: 'cover' }
}
}
},
experimental: {
typedPages: true,
},
})
app.vue Root Component
<!-- app.vue -->
<template>
<div>
<!-- Apply layout -->
<NuxtLayout>
<!-- Render page -->
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
// Global error handler
const { $toast } = useNuxtApp()
// Initialize global state
const authStore = useAuthStore()
await authStore.initialize()
</script>
Layout Configuration
<!-- layouts/default.vue -->
<template>
<div class="min-h-screen flex flex-col">
<TheHeader />
<main class="flex-1 container mx-auto px-4 py-8">
<slot />
</main>
<TheFooter />
<!-- Global toast notifications -->
<ToastContainer />
</div>
</template>
<script setup lang="ts">
// Layout-level SEO
useHead({
titleTemplate: (title) => title ? `${title} | Shop` : 'Shop',
})
</script>
<!-- layouts/checkout.vue -->
<template>
<div class="min-h-screen bg-gray-50">
<CheckoutHeader />
<main class="max-w-2xl mx-auto px-4 py-8">
<slot />
</main>
<!-- No footer on the checkout layout -->
</div>
</template>
TypeScript Configuration
Nuxt 3 supports TypeScript out of the box.
tsconfig.json
Nuxt automatically generates .nuxt/tsconfig.json, so the root tsconfig.json only needs to extend it.
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
Type Declaration Files
// types/index.ts
export interface User {
id: number
name: string
email: string
role: 'user' | 'admin'
createdAt: string
}
export interface Product {
id: number
name: string
price: number
description: string
images: string[]
stock: number
category: Category
}
export interface Category {
id: number
name: string
slug: string
}
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
// types/nuxt.d.ts — Extending Nuxt types
declare module 'nuxt/schema' {
interface RuntimeConfig {
stripe: {
secretKey: string
}
db: {
url: string
}
}
interface PublicRuntimeConfig {
appName: string
stripe: {
publishableKey: string
}
}
}
export {}
Expert Tips
1. Modularization with the Layers System
// nuxt.config.ts
export default defineNuxtConfig({
// Extend from other Nuxt projects or packages as layers
extends: [
'../shared-layer', // Shared components/composables
'github:my-org/nuxt-ui-kit', // A layer from GitHub
'@my-company/nuxt-auth', // An npm package layer
]
})
2. Customizing the Build Process with Hooks
// nuxt.config.ts
export default defineNuxtConfig({
hooks: {
// Runs after build is complete
'build:done'(nuxt) {
console.log('Build complete!')
},
// Runs when pages are generated
'pages:extend'(pages) {
// Add routes programmatically
pages.push({
name: 'custom-page',
path: '/custom',
file: '~/pages/custom.vue',
})
},
// Extend Vite config
'vite:extendConfig'(config, { isClient, isServer }) {
if (isClient) {
config.resolve!.alias!['#build/runtime'] = '~/runtime'
}
}
}
})
3. Distinguishing app.config.ts from Runtime Config
// app.config.ts — UI/UX settings (can be changed at runtime on the client)
export default defineAppConfig({
theme: {
primaryColor: '#3b82f6',
borderRadius: 'rounded-lg',
},
ui: {
notifications: {
position: 'top-right',
timeout: 3000,
}
}
})
<!-- Usage in components -->
<script setup lang="ts">
const appConfig = useAppConfig()
console.log(appConfig.theme.primaryColor) // '#3b82f6'
</script>
4. Branching Between Development and Production
// nuxt.config.ts
export default defineNuxtConfig({
$development: {
// Applied only in development
devtools: { enabled: true },
debug: true,
},
$production: {
// Applied only in production
nitro: {
compressPublicAssets: true,
}
}
})
Summary
Nuxt 3's environment setup may seem complex at first, but most settings have sensible defaults — you only need to override what you need in nuxt.config.ts.
Key takeaways:
- Directory structure: Each folder has a special role that Nuxt recognizes automatically
nuxt.config.ts: The central hub for all configuration; userouteRulesto set per-page rendering strategies- Environment variables: Managed safely through
runtimeConfig, with clear separation between server and client access - TypeScript: Supported out of the box; use type declarations for safer development
In the next chapter, we will look at Nuxt's core data fetching capabilities in detail.