Nuxt 4 + Nuxt UI Frontend + Backend API (Full Project)
This Word document includes both frontend pages and backend API server routes for a
simple product & category catalog built with Nuxt 4 and Nuxt UI.
Frontend: nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
nitro: {
storage: {
db: {
driver: 'fs',
base: './data',
},
},
},
})
Frontend: app.vue
<script setup lang="ts"></script>
<template>
<UApp>
<div class="min-h-screen flex flex-col">
<UHeader>
<template #left>
<NuxtLink to="/" class="font-semibold">Catalog
Demo</NuxtLink>
</template>
<template #right>
<div class="flex items-center gap-3">
<NuxtLink to="/products"
class="hover:underline">Products</NuxtLink>
<NuxtLink to="/categories"
class="hover:underline">Categories</NuxtLink>
</div>
</template>
</UHeader>
<UContainer class="py-6 flex-1">
<NuxtPage />
</UContainer>
<footer class="py-8 text-center text-sm opacity-70">Nuxt 4 + Nuxt
UI demo</footer>
</div>
</UApp>
</template>
Frontend: pages/products/index.vue
<script setup lang="ts">
import { useCatalogApi } from '@/composables/useCatalogApi'
import ProductCard from '@/components/ProductCard.vue'
const { fetchProducts } = useCatalogApi()
const products = ref([])
onMounted(async () => {
products.value = await fetchProducts()
})
</script>
<template>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Products</h1>
<UButton to="/products/create">New product</UButton>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</div>
</template>
Frontend: components/ProductForm.vue
<script setup lang="ts">
import type { Category, Product } from '@/types'
import { useCatalogApi } from '@/composables/useCatalogApi'
const emit = defineEmits<{ (e: 'submit', value: Omit<Product, 'id'>):
void }>()
const { fetchCategories } = useCatalogApi()
const categories = ref([])
onMounted(async () => {
categories.value = await fetchCategories()
})
const state = reactive<Omit<Product, 'id'>>({
name: '',
price: 0,
categoryId: undefined,
description: '',
})
</script>
<template>
<UForm @submit.prevent="() => emit('submit', state)">
<UFormField label="Name"><UInput v-model="state.name"
/></UFormField>
<UFormField label="Price"><UInput v-model.number="state.price"
type="number" step="0.01" /></UFormField>
<UFormField label="Category">
<USelect v-model="state.categoryId" :options="categories.map(c =>
({ label: c.name, value: c.id }))" />
</UFormField>
<UFormField label="Description"><UTextarea v-
model="state.description" /></UFormField>
<UButton type="submit">Create</UButton>
</UForm>
</template>
Backend: server/api/categories/index.ts
import { v4 as uuid } from 'uuid'
import type { Category } from '~/types'
export default defineEventHandler(async (event) => {
const storage = useStorage('db:categories.json')
const method = getMethod(event)
if (method === 'GET') {
return (await storage.getItem<Category[]>('categories')) || []
}
if (method === 'POST') {
const body = await readBody<Omit<Category, 'id'>>(event)
const categories = (await
storage.getItem<Category[]>('categories')) || []
const newCategory = { id: uuid(), ...body }
categories.push(newCategory)
await storage.setItem('categories', categories)
return newCategory
}
})
Backend: server/api/products/index.ts
import { v4 as uuid } from 'uuid'
import type { Product } from '~/types'
export default defineEventHandler(async (event) => {
const storage = useStorage('db:products.json')
const method = getMethod(event)
if (method === 'GET') {
return (await storage.getItem<Product[]>('products')) || []
}
if (method === 'POST') {
const body = await readBody<Omit<Product, 'id'>>(event)
const products = (await storage.getItem<Product[]>('products')) ||
[]
const newProduct = { id: uuid(), ...body }
products.push(newProduct)
await storage.setItem('products', products)
return newProduct
}
})