mgx

how to build an app with nuxt and bun

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 ``` ### Creating a Layout Create `app/layouts/default.vue`: ```vue ``` ### Working with Composables Create `app/composables/useCounter.ts`: ```typescript import { ref, computed } from 'vue' export const useCounter = (initialValue: number = 0) => { const count = ref(initialValue) const increment = () => count.value++ const decrement = () => count.value-- const reset = () => count.value = initialValue const isPositive = computed(() => count.value > 0) const doubled = computed(() => count.value * 2) return { count, increment, decrement, reset, isPositive, doubled } } ``` Use it in a component: ```vue ``` ## Building Components ### Creating a Reusable Component Create `app/components/UserCard.vue`: ```vue ``` ## 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 ``` ### 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 ``` ## 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 ``` ## 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!

Tagged in tech