This guide is a reflection of my experience moving a project from React/Next.js to Vue/Nuxt. I am relatively new to Nuxt, and I pieced this together by figuring out the process as I ported the existing application.
## Why Nuxt and Bun?
### Advantages of Using Nuxt with Bun
**Performance:**
- Bun is written in Zig and offers 3-4x faster package installation than npm
- Significantly faster development server startup
- Optimized for production builds with the Bun preset
**Developer Experience:**
- Single tool for package management, scripting, and running code
- Seamless TypeScript support out of the box
- Better error messages and debugging
**Compatibility:**
- Nuxt officially supports Bun as of version 3.6+ (Nuxt 4 also fully supports Bun)
- Bun preset available in Nitro (Nuxt's backend engine) for optimized production builds
- Full compatibility with Nuxt modules and ecosystem
## Prerequisites
Before you begin, ensure you have:
1. **Bun installed** (version 1.0 or higher)
```bash
curl -fsSL https://bun.sh/install | bash
```
2. **Basic knowledge of:**
- Vue 3 and Composition API
- TypeScript (recommended but optional)
- Modern JavaScript (ES6+)
3. **A code editor** (VS Code recommended with Volar extension)
## Getting Started
### Step 1: Initialize a New Nuxt Project with Bun
Using the official `nuxi` CLI with Bun:
```bash
bun x nuxi init my-nuxt-app
```
**Note:** You can also use `bunx` (which is an alias for `bun x`), but `bun x` is the recommended syntax.
When prompted:
1. Select a template (e.g., **`minimal`** for a minimal setup, or **`ui`** for Nuxt UI)
2. Select **`bun`** as your package manager:
```bash
✔ Which package manager would you like to use?
› bun
◐ Installing dependencies...
bun install v1.3.3 (16b4bf34)
+ @nuxt/devtools@0.8.2
+ nuxt@4.2.1
785 packages installed [2.67s]
✔ Installation completed.
✔ Types generated in .nuxt
✨ Nuxt project has been created with the v3 template.
Next steps:
› cd my-nuxt-app
› Start development server with bun run dev
```
### Step 2: Navigate to Your Project
```bash
cd my-nuxt-app
```
### Step 3: Start the Development Server
To run the development server with Bun runtime (recommended for best performance):
```bash
bun --bun run dev
```
The `--bun` flag forces the development server to use the Bun runtime instead of Node.js.
Expected output:
```bash
$ bun --bun run dev
nuxt dev
Nuxt 4.2.1
Nuxt 4.2.1 (with Nitro 2.12.9, Vite 7.2.6 and Vue 3.5.25)
> Local: http://localhost:3000/
> Network: http://192.168.0.21:3000/
➜ DevTools: press Shift + Option + D in the browser (v3.1.1)
✔ Vite client built in 19ms
✔ Vite server built in 10ms
✔ Nuxt Nitro server built in 186ms
ℹ Vite server warmed up in 1ms
ℹ Vite client warmed up in 1ms
```
Open your browser to `http://localhost:3000` to see the welcome screen!
## Project Structure
After initialization, your Nuxt project has this structure:
```
my-nuxt-app/
├── app/
│ ├── app.vue # Root component
│ ├── components/ # Vue components (auto-imported)
│ ├── pages/ # File-based routing
│ ├── layouts/ # Layout components
│ ├── middleware/ # Route middleware
│ ├── plugins/ # Plugins
│ ├── app.config.ts # App configuration
│ └── error.vue # Error page
├── server/
│ ├── api/ # Server API routes
│ ├── middleware/ # Server middleware
│ ├── utils/ # Server utilities
│ └── tsconfig.json # Server TypeScript config
├── public/ # Static assets
├── nuxt.config.ts # Nuxt configuration
├── package.json # Dependencies
├── tsconfig.json # TypeScript configuration
└── bunfig.toml # Bun configuration (optional)
```
### Understanding Key Directories
**`app/components/`** - Auto-imported Vue components. No explicit imports needed:
```vue
```
**`app/pages/`** - File-based routing. Create `app/pages/blog.vue` → accessible at `/blog`
**`app/layouts/`** - Reusable layout wrappers for pages
**`server/api/`** - Server-side API routes. Create `server/api/users.ts` → accessible at `/api/users`
## Development
### Creating Your First Page
Create `app/pages/index.vue`:
```vue
Welcome to Nuxt + Bun!
Counter: {{ counter }}
```
### Creating a Layout
Create `app/layouts/default.vue`:
```vue
```
## Building Components
### Creating a Reusable Component
Create `app/components/UserCard.vue`:
```vue
{{ user.name }}
{{ user.email }}
```
## API Routes & Server Middleware
### Creating API Routes
**Important:** When using `await` inside an event handler (e.g., with `readBody`), the handler function must be marked as `async`.
Create `server/api/users.ts`:
```typescript
// Mock database
const users = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com' },
{ id: 3, name: 'Carol White', email: 'carol@example.com' }
]
export default defineEventHandler(async (event) => {
const query = getQuery(event)
// GET /api/users - Get all users
if (event.node.req.method === 'GET') {
if (query.id) {
const user = users.find(u => u.id === parseInt(query.id as string))
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return user
}
return users
}
// POST /api/users - Create user
if (event.node.req.method === 'POST') {
const body = await readBody(event)
const newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
...body
}
users.push(newUser)
return newUser
}
throw createError({
statusCode: 405,
statusMessage: 'Method not allowed'
})
})
```
Create `server/api/users/[id].ts` for individual user routes:
```typescript
const users = [
{ id: 1, name: 'Alice Johnson', email: 'alice@example.com' },
{ id: 2, name: 'Bob Smith', email: 'bob@example.com' },
{ id: 3, name: 'Carol White', email: 'carol@example.com' }
]
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
if (event.node.req.method === 'GET') {
const user = users.find(u => u.id === parseInt(id as string))
if (!user) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
return user
}
if (event.node.req.method === 'PUT') {
const userIndex = users.findIndex(u => u.id === parseInt(id as string))
if (userIndex === -1) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
const body = await readBody(event)
users[userIndex] = { ...users[userIndex], ...body }
return users[userIndex]
}
if (event.node.req.method === 'DELETE') {
const userIndex = users.findIndex(u => u.id === parseInt(id as string))
if (userIndex === -1) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}
const deletedUser = users.splice(userIndex, 1)
return { success: true, user: deletedUser[0] }
}
throw createError({
statusCode: 405,
statusMessage: 'Method not allowed'
})
})
```
### Using API Routes from Components
Create `app/pages/users.vue`:
```vue
Users
Loading...
{{ error }}
{{ user.name }}
{{ user.email }}
No users found
```
### Server Middleware
Create `server/middleware/auth.ts` for request logging:
```typescript
export default defineEventHandler((event) => {
// Log incoming requests
console.log(`[${new Date().toISOString()}] ${event.node.req.method} ${event.node.req.url}`)
// Add custom header
setResponseHeader(event, 'X-Custom-Header', 'Nuxt-Bun-App')
})
```
## State Management with Pinia
Pinia is a store library for Vue, it allows you to share a state across components/pages.
### Installing Pinia
```bash
bun add pinia @pinia/nuxt
```
Then add it to your `nuxt.config.ts`:
```typescript
export default defineNuxtConfig({
modules: ['@pinia/nuxt']
})
```
### Setting up Pinia Store
Create `app/stores/user.ts`:
```typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
export const useUserStore = defineStore('user', () => {
const currentUser = ref(null)
const isAuthenticated = computed(() => currentUser.value !== null)
const setUser = (user: User | null) => {
currentUser.value = user
}
const logout = () => {
currentUser.value = null
}
const fetchUser = async (userId: number) => {
try {
const user = await $fetch(`/api/users/${userId}`)
setUser(user)
return user
} catch (err) {
console.error('Failed to fetch user:', err)
return null
}
}
return {
currentUser,
isAuthenticated,
setUser,
logout,
fetchUser
}
})
```
### Using Pinia in Components
```vue
Welcome, {{ userStore.currentUser?.name }}!
Not logged in
```
## Production Build
### Building for Production
With Bun preset (recommended for optimal performance):
```bash
NITRO_PRESET=bun bun run build
```
Or configure in `nuxt.config.ts`:
```typescript
export default defineNuxtConfig({
nitro: {
preset: 'bun'
}
})
```
Then build:
```bash
bun run build
```
### Output Structure
After building, you'll have:
```
.output/
├── public/ # Static assets
├── server/
│ └── index.mjs # Server entry point
└── nitro.json # Configuration
```
### Running Production Build
```bash
bun run .output/server/index.mjs
```
The app will be available at `http://localhost:3000` (or your configured port).
### Environment Variables
Create `.env` for development:
```env
NUXT_PUBLIC_API_BASE=http://localhost:3000
NODE_ENV=development
```
Create `.env.production` for production:
```env
NUXT_PUBLIC_API_BASE=https://api.yourdomain.com
NODE_ENV=production
```
Access in components:
```typescript
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
```
## Best Practices
### 1. Use TypeScript Everywhere
Always define types for better development experience:
```typescript
// Good
interface Product {
id: number
name: string
price: number
}
const products: Ref = ref([])
// Avoid
const products = ref([])
```
### 2. Organize Your Components
Use atomic design or feature-based structure:
```
components/
├── atoms/
│ ├── BaseButton.vue
│ ├── BaseInput.vue
│ └── BaseCard.vue
├── molecules/
│ ├── UserForm.vue
│ └── SearchBar.vue
└── organisms/
├── UserList.vue
└── Navigation.vue
```
### 3. Use Composables for Logic
Extract reusable logic into composables:
```typescript
// composables/useFetch.ts
export const useFetchData = async (url: string) => {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
try {
data.value = await $fetch(url)
} catch (err) {
error.value = err
} finally {
loading.value = false
}
return { data, error, loading }
}
```
### 4. Use Route Middleware for Authentication
Create `app/middleware/auth.ts`:
```typescript
export default defineRouteMiddleware((to, from) => {
const userStore = useUserStore()
if (!userStore.isAuthenticated && to.path !== '/login') {
return navigateTo('/login')
}
})
```
Use in pages:
```vue
```
### 5. Lazy Load Routes
For large applications, lazy load routes:
```typescript
// app/pages/dashboard.vue - Will be lazy loaded
defineRouteOptions({ lazy: true })
```
### 6. Optimize Images
Use Nuxt Image component:
```vue
```
### 7. Error Handling
Create a custom error page `app/error.vue`:
```vue
{{ error.statusCode }}
{{ error.message }}
```
## Troubleshooting
### Issue: "await can only be used inside an async function" error
**Solution:** When using `await` in API route handlers (e.g., `await readBody(event)`), make sure the handler function is marked as `async`:
```typescript
// Correct
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// ...
})
// Incorrect
export default defineEventHandler((event) => {
const body = await readBody(event) // Error!
// ...
})
```
### Issue: "Module not found" errors with Bun
**Solution:** Ensure you're using the `--bun` flag:
```bash
bun --bun run dev
```
### Issue: TypeScript errors with auto-imported components
**Solution:** Update your `tsconfig.json`:
```json
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strict": true,
"moduleResolution": "bundler"
}
}
```
### Issue: Hot Module Replacement (HMR) not working
**Solution:** Add to `nuxt.config.ts`:
```typescript
export default defineNuxtConfig({
vite: {
server: {
hmr: {
protocol: 'ws',
host: 'localhost',
port: 24678
}
}
}
})
```
### Issue: API routes returning 404 in production
**Solution:** Ensure Nitro is configured correctly:
```typescript
export default defineNuxtConfig({
nitro: {
preset: 'bun',
minify: true
}
})
```
### Issue: Database connections in production
**Solution:** Use environment variables and ensure connections are pooled:
```typescript
export default defineEventHandler(async (event) => {
const db = useDB() // Custom server utility
const data = await db.query('SELECT * FROM users')
return data
})
```
## Quick Command Reference
```bash
# Initialize project
bun x nuxi init my-app
# Install dependencies
bun install
# Development (with Bun runtime)
bun --bun run dev
# Development (with Node.js runtime)
bun run dev
# Build for production
NITRO_PRESET=bun bun run build
# Start production server
bun run .output/server/index.mjs
# Generate types
bun run generate
# Analyze bundle
bun run analyze
# Type check
bun run typecheck
```
For more information, visit:
- [Nuxt Documentation](https://nuxt.com)
- [Bun Documentation](https://bun.com)
- [Bun + Nuxt Guide](https://bun.com/docs/guides/ecosystem/nuxt)
Happy coding!