Bookmark
Nuxt 3 Full-Stack Template
assets/css/main.css
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
:root {
/* Light theme */
--color-background: #ffffff;
--color-background-soft: #f8fafc;
--color-background-mute: #f1f5f9;
--color-border: #e2e8f0;
--color-border-hover: #cbd5e1;
--color-heading: #1e293b;
--color-text: #475569;
--color-text-soft: #64748b;
--color-primary: #3b82f6;
--color-primary-hover: #2563eb;
--color-primary-soft: #dbeafe;
--color-secondary: #6366f1;
--color-success: #10b981;
--color-warning: #f59e0b;
--color-error: #ef4444;
}
.dark {
--color-background: #0f172a;
--color-background-soft: #1e293b;
--color-background-mute: #334155;
--color-border: #475569;
--color-border-hover: #64748b;
--color-heading: #f1f5f9;
--color-text: #cbd5e1;
--color-text-soft: #94a3b8;
--color-primary: #60a5fa;
--color-primary-hover: #3b82f6;
--color-primary-soft: #1e3a8a;
--color-secondary: #818cf8;
--color-success: #34d399;
--color-warning: #fbbf24;
--color-error: #f87171;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--color-text);
background: var(--color-background);
transition: color 0.3s ease, background-color 0.3s ease;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background-soft);
}
::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-border-hover);
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Focus styles */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Loading spinner */
.spinner {
@apply inline-block w-4 h-4 border-2 border-current border-r-transparent rounded-full animate-spin;
}
app.vue
<template>
<Html :lang="locale" :class="{ dark: $colorMode.value === 'dark' }">
<Head>
<Title>{{ title }}</Title>
<Meta name="description" :content="description" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Link rel="icon" type="image/x-icon" href="/favicon.ico" />
</Head>
<Body class="antialiased">
<div id="app" class="min-h-screen bg-background text-text">
<AppHeader />
<main class="flex-1">
<NuxtPage />
</main>
<AppFooter />
<!-- Global modals and overlays -->
<LazyUNotifications />
<UModals />
</div>
</Body>
</Html>
</template>
<script setup lang="ts">
const { locale } = useI18n()
const colorMode = useColorMode()
// SEO
const title = 'Nuxt 3 App'
const description = 'A modern, full-stack Nuxt 3 application with TypeScript and best practices'
// Global error handling
const { $router } = useNuxtApp()
onErrorCaptured((error) => {
console.error('Global error captured:', error)
// You can send error to logging service here
return false
})
// PWA
if (process.client) {
// Register service worker
navigator.serviceWorker?.register('/sw.js')
}
</script>
layouts/default.vue
<template>
<div class="min-h-screen flex flex-col">
<AppHeader />
<main class="flex-1 container mx-auto px-4 py-8">
<slot />
</main>
<AppFooter />
</div>
</template>
<script setup lang="ts">
// Layout-specific logic can go here
</script>
layouts/auth.vue
<template>
<div class="min-h-screen flex items-center justify-center bg-background-soft">
<div class="w-full max-w-md">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
// Auth layout for login/register pages
</script>
components/AppHeader.vue
<template>
<header class="sticky top-0 z-50 bg-background/80 backdrop-blur-sm border-b border-border">
<nav class="container mx-auto px-4 py-4">
<div class="flex items-center justify-between">
<!-- Logo -->
<NuxtLink to="/" class="flex items-center space-x-2">
<Icon name="lucide:zap" class="w-8 h-8 text-primary" />
<span class="text-xl font-bold text-heading">Nuxt App</span>
</NuxtLink>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-6">
<NuxtLink
v-for="item in navigation"
:key="item.to"
:to="item.to"
class="text-text hover:text-primary transition-colors duration-200"
active-class="text-primary font-semibold"
>
{{ item.label }}
</NuxtLink>
</div>
<!-- Actions -->
<div class="flex items-center space-x-3">
<!-- Theme Toggle -->
<UButton
:icon="$colorMode.value === 'dark' ? 'lucide:sun' : 'lucide:moon'"
color="gray"
variant="ghost"
size="sm"
@click="$colorMode.preference = $colorMode.value === 'dark' ? 'light' : 'dark'"
/>
<!-- Language Switcher -->
<UDropdown
:items="localeItems"
:popper="{ placement: 'bottom-end' }"
>
<UButton
:label="locale.toUpperCase()"
trailing-icon="lucide:chevron-down"
color="gray"
variant="ghost"
size="sm"
/>
</UDropdown>
<!-- User Menu -->
<UDropdown
v-if="user"
:items="userMenuItems"
:popper="{ placement: 'bottom-end' }"
>
<UAvatar
:src="user.avatar"
:alt="user.name"
size="sm"
class="cursor-pointer"
/>
</UDropdown>
<!-- Auth Buttons -->
<div v-else class="hidden md:flex items-center space-x-2">
<UButton to="/auth/login" variant="ghost" size="sm">
Login
</UButton>
<UButton to="/auth/register" size="sm">
Sign Up
</UButton>
</div>
<!-- Mobile Menu Toggle -->
<UButton
:icon="isMenuOpen ? 'lucide:x' : 'lucide:menu'"
color="gray"
variant="ghost"
size="sm"
class="md:hidden"
@click="isMenuOpen = !isMenuOpen"
/>
</div>
</div>
<!-- Mobile Menu -->
<div
v-if="isMenuOpen"
class="md:hidden mt-4 pb-4 border-t border-border pt-4 space-y-2"
>
<NuxtLink
v-for="item in navigation"
:key="item.to"
:to="item.to"
class="block py-2 text-text hover:text-primary transition-colors"
@click="isMenuOpen = false"
>
{{ item.label }}
</NuxtLink>
<div v-if="!user" class="pt-4 space-y-2">
<UButton to="/auth/login" variant="ghost" block>
Login
</UButton>
<UButton to="/auth/register" block>
Sign Up
</UButton>
</div>
</div>
</nav>
</header>
</template>
<script setup lang="ts">
const { locale, locales, setLocale } = useI18n()
const colorMode = useColorMode()
const { user, logout } = useAuth()
const isMenuOpen = ref(false)
const navigation = [
{ label: 'Home', to: '/' },
{ label: 'About', to: '/about' },
{ label: 'Blog', to: '/blog' },
{ label: 'Contact', to: '/contact' }
]
const localeItems = computed(() => [
locales.value.map((locale: any) => ({
label: locale.name,
click: () => setLocale(locale.code)
}))
])
const userMenuItems = computed(() => [
[{
label: 'Profile',
icon: 'lucide:user',
to: '/profile'
}, {
label: 'Settings',
icon: 'lucide:settings',
to: '/settings'
}],
[{
label: 'Logout',
icon: 'lucide:log-out',
click: logout
}]
])
// Close mobile menu on route change
watch(() => useRoute().path, () => {
isMenuOpen.value = false
})
</script>
components/AppFooter.vue
<template>
<footer class="bg-background-soft border-t border-border">
<div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<!-- Company Info -->
<div class="space-y-4">
<div class="flex items-center space-x-2">
<Icon name="lucide:zap" class="w-6 h-6 text-primary" />
<span class="text-lg font-bold text-heading">Nuxt App</span>
</div>
<p class="text-text-soft text-sm">
Building modern web applications with Nuxt 3, TypeScript, and best practices.
</p>
<div class="flex space-x-4">
<UButton
to="https://github.com"
target="_blank"
icon="lucide:github"
color="gray"
variant="ghost"
size="sm"
/>
<UButton
to="https://twitter.com"
target="_blank"
icon="lucide:twitter"
color="gray"
variant="ghost"
size="sm"
/>
<UButton
to="https://linkedin.com"
target="_blank"
icon="lucide:linkedin"
color="gray"
variant="ghost"
size="sm"
/>
</div>
</div>
<!-- Quick Links -->
<div class="space-y-4">
<h3 class="font-semibold text-heading">Quick Links</h3>
<ul class="space-y-2">
<li v-for="link in quickLinks" :key="link.to">
<NuxtLink
:to="link.to"
class="text-text-soft hover:text-primary transition-colors text-sm"
>
{{ link.label }}
</NuxtLink>
</li>
</ul>
</div>
<!-- Resources -->
<div class="space-y-4">
<h3 class="font-semibold text-heading">Resources</h3>
<ul class="space-y-2">
<li v-for="resource in resources" :key="resource.to">
<NuxtLink
:to="resource.to"
class="text-text-soft hover:text-primary transition-colors text-sm"
target="_blank"
>
{{ resource.label }}
</NuxtLink>
</li>
</ul>
</div>
<!-- Newsletter -->
<div class="space-y-4">
<h3 class="font-semibold text-heading">Stay Updated</h3>
<p class="text-text-soft text-sm">
Subscribe to our newsletter for the latest updates.
</p>
<UForm :state="newsletter" @submit="subscribeNewsletter">
<div class="flex space-x-2">
<UInput
v-model="newsletter.email"
type="email"
placeholder="Enter your email"
size="sm"
class="flex-1"
/>
<UButton type="submit" size="sm">
Subscribe
</UButton>
</div>
</UForm>
</div>
</div>
<!-- Copyright -->
<div class="mt-8 pt-8 border-t border-border">
<div class="flex flex-col md:flex-row justify-between items-center space-y-2 md:space-y-0">
<p class="text-text-soft text-sm">
© {{ currentYear }} Nuxt App. All rights reserved.
</p>
<div class="flex space-x-6">
<NuxtLink to="/privacy" class="text-text-soft hover:text-primary text-sm">
Privacy Policy
</NuxtLink>
<NuxtLink to="/terms" class="text-text-soft hover:text-primary text-sm">
Terms of Service
</NuxtLink>
</div>
</div>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
const currentYear = new Date().getFullYear()
const newsletter = reactive({
email: ''
})
const quickLinks = [
{ label: 'Home', to: '/' },
{ label: 'About', to: '/about' },
{ label: 'Blog', to: '/blog' },
{ label: 'Contact', to: '/contact' }
]
const resources = [
{ label: 'Documentation', to: 'https://nuxt.com/docs' },
{ label: 'Examples', to: 'https://nuxt.com/examples' },
{ label: 'Community', to: 'https://nuxt.com/community' },
{ label: 'Support', to: 'https://nuxt.com/support' }
]
const subscribeNewsletter = async () => {
try {
// Handle newsletter subscription
const { $fetch } = useNuxtApp()
await $fetch('/api/newsletter/subscribe', {
method: 'POST',
body: { email: newsletter.email }
})
const toast = useToast()
toast.add({
title: 'Success!',
description: 'Thank you for subscribing to our newsletter.',
color: 'green'
})
newsletter.email = ''
} catch (error) {
const toast = useToast()
toast.add({
title: 'Error',
description: 'Failed to subscribe. Please try again.',
color: 'red'
})
}
}
</script>
pages/index.vue
<template>
<div>
<!-- Hero Section -->
<section class="py-20 text-center">
<div class="max-w-4xl mx-auto">
<h1 class="text-5xl md:text-6xl font-bold text-heading mb-6">
Welcome to
<span class="text-primary">Nuxt 3</span>
</h1>
<p class="text-xl text-text-soft mb-8 max-w-2xl mx-auto">
A modern, full-stack application template built with Nuxt 3, TypeScript, and Tailwind CSS.
Perfect for building scalable web applications.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<UButton size="lg" to="/about">
Get Started
</UButton>
<UButton size="lg" variant="outline" to="https://nuxt.com" target="_blank">
Learn Nuxt
</UButton>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-20 bg-background-soft">
<div class="container mx-auto px-4">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-heading mb-4">
Modern Features
</h2>
<p class="text-text-soft max-w-2xl mx-auto">
Built with the latest technologies and best practices for modern web development.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<FeatureCard
v-for="feature in features"
:key="feature.title"
:icon="feature.icon"
:title="feature.title"
:description="feature.description"
/>
</div>
</div>
</section>
<!-- Stats Section -->
<section class="py-20">
<div class="container mx-auto px-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-8 text-center">
<div v-for="stat in stats" :key="stat.label">
<div class="text-3xl font-bold text-primary mb-2">
{{ stat.value }}
</div>
<div class="text-text-soft">
{{ stat.label }}
</div>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-20 bg-primary text-white">
<div class="container mx-auto px-4 text-center">
<h2 class="text-3xl font-bold mb-4">
Ready to Get Started?
</h2>
<p class="text-xl mb-8 opacity-90">
Start building your next application with our modern template.
</p>
<UButton size="lg" color="white" variant="solid" to="/contact">
Contact Us
</UButton>
</div>
</section>
</div>
</template>
<script setup lang="ts">
// SEO
definePageMeta({
title: 'Home - Nuxt 3 App',
description: 'Welcome to our modern Nuxt 3 application template'
})
const features = [
{
icon: 'lucide:zap',
title: 'Lightning Fast',
description: 'Built with Nuxt 3 for optimal performance and developer experience.'
},
{
icon: 'lucide:shield-check',
title: 'Type Safe',
description: 'Full TypeScript support with strict type checking and IntelliSense.'
},
{
icon: 'lucide:palette',
title: 'Beautiful UI',
description: 'Styled with Tailwind CSS and Nuxt UI for modern, responsive design.'
},
{
icon: 'lucide:server',
title: 'Full-Stack',
description: 'Server-side rendering, API routes, and database integration ready.'
},
{
icon: 'lucide:smartphone',
title: 'Mobile First',
description: 'Responsive design that works perfectly on all devices and screen sizes.'
},
{
icon: 'lucide:layers',
title: 'Modular',
description: 'Clean architecture with reusable components and composables.'
}
]
const stats = [
{ value: '100+', label: 'Components' },
{ value: '99%', label: 'Uptime' },
{ value: '50+', label: 'Features' },
{ value: '24/7', label: 'Support' }
]
</script>
pages/about.vue
<template>
<div class="max-w-4xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-heading mb-4">
About Our Application
</h1>
<p class="text-xl text-text-soft">
Learn more about our modern Nuxt 3 application and the technologies behind it.
</p>
</div>
<div class="prose prose-lg mx-auto">
<section class="mb-12">
<h2 class="text-2xl font-semibold text-heading mb-4">Our Mission</h2>
<p class="text-text leading-relaxed mb-6">
We believe in building modern, scalable web applications that provide exceptional user experiences.
Our Nuxt 3 template combines the latest technologies with best practices to help developers
create production-ready applications quickly and efficiently.
</p>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-heading mb-4">Technology Stack</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<TechCard
v-for="tech in technologies"
:key="tech.name"
:name="tech.name"
:description="tech.description"
:icon="tech.icon"
:link="tech.link"
/>
</div>
</section>
<section class="mb-12">
<h2 class="text-2xl font-semibold text-heading mb-4">Key Features</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="feature in keyFeatures" :key="feature" class="flex items-center space-x-2">
<Icon name="lucide:check-circle" class="w-5 h-5 text-success" />
<span class="text-text">{{ feature }}</span>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup lang="ts">
// SEO
definePageMeta({
title: 'About - Nuxt 3 App',
description: 'Learn about our modern Nuxt 3 application and technology stack'
})
const technologies = [
{
name: 'Nuxt 3',
description: 'The intuitive Vue framework for building modern web applications',
icon: 'simple-icons:nuxtdotjs',
link: 'https://nuxt.com'
},
{
name: 'TypeScript',
description: 'Strongly typed programming language that builds on JavaScript',
icon: 'simple-icons:typescript',
link: 'https://typescriptlang.org'
},
{
name: 'Tailwind CSS',
description: 'Utility-first CSS framework for rapid UI development',
icon: 'simple-icons:tailwindcss',
link: 'https://tailwindcss.com'
},
{
name: 'Nuxt UI',
description: 'Beautiful and accessible components built for Nuxt',
icon: 'simple-icons:nuxtdotjs',
link: 'https://ui.nuxt.com'
}
]
const keyFeatures = [
'Server-Side Rendering (SSR)',
'Static Site Generation (SSG)',
'Auto-imports for components and composables',
'File-based routing system',
'Built-in SEO optimization',
'TypeScript support out of the box',
'Responsive design with Tailwind CSS',
'Dark mode support',
'Internationalization (i18n)',
'Progressive Web App (PWA) ready',
'API routes and middleware',
'Database integration ready'
]
</script>
pages/blog/index.vue
<template>
<div>
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-heading mb-4">
Blog
</h1>
<p class="text-xl text-text-soft">
Latest articles and insights about web development.
</p>
</div>
<!-- Search and Filters -->
<div class="mb-8 flex flex-col sm:flex-row gap-4">
<UInput
v-model="searchQuery"
placeholder="Search articles..."
icon="lucide:search"
class="flex-1"
/>
<USelectMenu
v-model="selectedCategory"
:options="categories"
placeholder="All Categories"
class="w-full sm:w-48"
/>
</div>
<!-- Articles Grid -->
<div v-if="filteredArticles.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<ArticleCard
v-for="article in filteredArticles"
:key="article.slug"
:article="article"
/>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12">
<Icon name="lucide:file-text" class="w-16 h-16 text-text-soft mx-auto mb-4" />
<h3 class="text-xl font-semibold text-heading mb-2">No articles found</h3>
<p class="text-text-soft">Try adjusting your search or filter criteria.</p>
</div>
<!-- Pagination -->
<div v-if="totalPages > 1" class="mt-12 flex justify-center">
<UPagination
v-model="currentPage"
:page-count="itemsPerPage"
:total="filteredArticles.length"
/>
</div>
</div>
</template>
<script setup lang="ts">
// SEO
definePageMeta({
title: 'Blog - Nuxt 3 App',
description: 'Read our latest articles and insights about web development'
})
const searchQuery = ref('')
const selectedCategory = ref('')
const currentPage = ref(1)
const itemsPerPage = 9
// Mock articles data
const articles = ref([
{
slug: 'getting-started-nuxt3',
title: 'Getting Started with Nuxt 3',
description: 'Learn how to build modern web applications with Nuxt 3',
category: 'Tutorial',
author: 'John Doe',
publishedAt: '2025-01-15',
image: '/images/blog/nuxt3-intro.jpg',
readTime: '5 min read'
},
{
slug: 'typescript-best-practices',
title: 'TypeScript Best Practices',
description: 'Essential TypeScript patterns for better code quality',
category: 'Development',
author: 'Jane Smith',
publishedAt: '2025-01-10',
image: '/images/blog/typescript-tips.jpg',
readTime: '8 min read'
},
{
slug: 'tailwind-css-tips',
title: 'Advanced Tailwind CSS Techniques',
description: 'Pro tips for building beautiful UIs with Tailwind CSS',
category: 'Design',
author: 'Mike Johnson',
publishedAt: '2025-01-05',
image: '/images/blog/tailwind-advanced.jpg',
readTime: '6 min read'
}
])
const categories = computed(() => [
{ label: 'All Categories', value: '' },
...Array.from(new Set(articles.value.map(a => a.category)))
.map(category => ({ label: category, value: category }))
])
const filteredArticles = computed(() => {
let filtered = articles.value
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
filtered = filtered.filter(article =>
article.title.toLowerCase().includes(query) ||
article.description.toLowerCase().includes(query)
)
}
if (selectedCategory.value) {
filtered = filtered.filter(article => article.category === selectedCategory.value)
}
return filtered
})
const totalPages = computed(() => Math.ceil(filteredArticles.value.length / itemsPerPage))
</script>
pages/contact.vue
<template>
<div class="max-w-2xl mx-auto">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-heading mb-4">
Contact Us
</h1>
<p class="text-xl text-text-soft">
Get in touch with our team. We'd love to hear from you.
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12">
<!-- Contact Form -->
<div>
<UForm
ref="form"
:schema="schema"
:state="formData"
@submit="submitForm"
class="space-y-6"
>
<UFormGroup label="Name" name="name" required>
<UInput
v-model="formData.name"
placeholder="Your full name"
icon="lucide:user"
/>
</UFormGroup>
<UFormGroup label="Email" name="email" required>
<UInput
v-model="formData.email"
type="email"
placeholder="your.email@example.com"
icon="lucide:mail"
/>
</UFormGroup>
<UFormGroup label="Subject" name="subject" required>
<UInput
v-model="formData.subject"
placeholder="What's this about?"
icon="lucide:message-square"
/>
</UFormGroup>
<UFormGroup label="Message" name="message" required>
<UTextarea
v-model="formData.message"
placeholder="Tell us more about your inquiry..."
:rows="6"
/>
</UFormGroup>
<UButton
type="submit"
:loading="isSubmitting"
block
size="lg"
>
Send Message
</UButton>
</UForm>
</div>
<!-- Contact Information -->
<div class="space-y-8">
<div>
<h3 class="text-xl font-semibold text-heading mb-4">
Get in Touch
</h3>
<p class="text-text-soft mb-6">
Have questions about our application or need support?
We're here to help and would love to hear from you.
</p>
</div>
<div class="space-y-4">
<ContactInfo
icon="lucide:mail"
label="Email"
value="hello@nuxtapp.com"
link="mailto:hello@nuxtapp.com"
/>
<ContactInfo
icon="lucide:phone"
label="Phone"
value="+1 (555) 123-4567"
link="tel:+15551234567"
/>
<ContactInfo
icon="lucide:map-pin"
label="Address"
value="123 Developer Street, Tech City, TC 12345"
/>
</div>
<div>
<h4 class="font-semibold text-heading mb-3">Follow Us</h4>
<div class="flex space-x-3">
<UButton
to="https://github.com"
target="_blank"
icon="lucide:github"
color="gray"
variant="ghost"
size="sm"
/>
<UButton
to="https://twitter.com"
target="_blank"
icon="lucide:twitter"
color="gray"
variant="ghost"
size="sm"
/>
<UButton
to="https://linkedin.com"
target="_blank"
icon="lucide:linkedin"
color="gray"
variant="ghost"
size="sm"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { z } from 'zod'
// SEO
definePageMeta({
title: 'Contact - Nuxt 3 App',
description: 'Get in touch with our team. We\'d love to hear from you.'
})
const isSubmitting = ref(false)
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Please enter a valid email address'),
subject: z.string().min(5, 'Subject must be at least 5 characters'),
message: z.string().min(10, 'Message must be at least 10 characters')
})
const formData = reactive({
name: '',
email: '',
subject: '',
message: ''
})
const submitForm = async () => {
isSubmitting.value = true
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000))
const toast = useToast()
toast.add({
title: 'Message Sent!',
description: 'Thank you for your message. We\'ll get back to you soon.',
color: 'green'
})
// Reset form
Object.assign(formData, {
name: '',
email: '',
subject: '',
message: ''
})
} catch (error) {
const toast = useToast()
toast.add({
title: 'Error',
description: 'Failed to send message. Please try again.',
color: 'red'
})
} finally {
isSubmitting.value = false
}
}
</script>
components/FeatureCard.vue
<template>
<div class="p-6 bg-background rounded-lg border border-border hover:border-border-hover transition-colors duration-200">
<div class="mb-4">
<Icon :name="icon" class="w-8 h-8 text-primary" />
</div>
<h3 class="text-lg font-semibold text-heading mb-2">
{{ title }}
</h3>
<p class="text-text-soft">
{{ description }}
</p>
</div>
</template>
<script setup lang="ts">
interface Props {
icon: string
title: string
description: string
}
defineProps<Props>()
</script>
components/TechCard.vue
<template>
<NuxtLink
:to="link"
target="_blank"
class="p-4 bg-background-soft rounded-lg border border-border hover:border-border-hover transition-colors duration-200 group"
>
<div class="flex items-center space-x-3 mb-2">
<Icon :name="icon" class="w-6 h-6 text-primary" />
<h3 class="font-semibold text-heading group-hover:text-primary transition-colors">
{{ name }}
</h3>
</div>
<p class="text-text-soft text-sm">
{{ description }}
</p>
</NuxtLink>
</template>
<script setup lang="ts">
interface Props {
name: string
description: string
icon: string
link: string
}
defineProps<Props>()
</script>
components/ArticleCard.vue
<template>
<NuxtLink :to="`/blog/${article.slug}`" class="group">
<article class="bg-background rounded-lg border border-border overflow-hidden hover:border-border-hover transition-colors duration-200">
<div class="aspect-video bg-background-mute overflow-hidden">
<img
v-if="article.image"
:src="article.image"
:alt="article.title"
class="size-full object-cover group-hover:scale-105 transition-transform duration-200"
>
<div v-else class="size-full flex items-center justify-center">
<Icon name="lucide:image" class="w-12 h-12 text-text-soft" />
</div>
</div>
<div class="p-6">
<div class="flex items-center justify-between text-sm text-text-soft mb-2">
<span class="bg-primary/10 text-primary px-2 py-1 rounded-full">
{{ article.category }}
</span>
<span>{{ article.readTime }}</span>
</div>
<h3 class="text-lg font-semibold text-heading mb-2 group-hover:text-primary transition-colors">
{{ article.title }}
</h3>
<p class="text-text-soft mb-4 line-clamp-2">
{{ article.description }}
</p>
<div class="flex items-center justify-between text-sm text-text-soft">
<span>{{ article.author }}</span>
<time :datetime="article.publishedAt">
{{ formatDate(article.publishedAt) }}
</time>
</div>
</div>
</article>
</NuxtLink>
</template>
<script setup lang="ts">
interface Article {
slug: string
title: string
description: string
category: string
author: string
publishedAt: string
image?: string
readTime: string
}
interface Props {
article: Article
}
defineProps<Props>()
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
}
</script>
components/ContactInfo.vue
<template>
<div class="flex items-start space-x-3">
<Icon :name="icon" class="w-5 h-5 text-primary mt-0.5" />
<div>
<div class="font-medium text-heading">{{ label }}</div>
<NuxtLink
v-if="link"
:to="link"
class="text-text-soft hover:text-primary transition-colors"
>
{{ value }}
</NuxtLink>
<div v-else class="text-text-soft">
{{ value }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
icon: string
label: string
value: string
link?: string
}
defineProps<Props>()
</script>
components/ui/LoadingSpinner.vue
<template>
<div class="flex items-center justify-center" :class="containerClass">
<div
class="animate-spin rounded-full border-2 border-current border-r-transparent"
:class="spinnerClass"
/>
<span v-if="text" class="ml-3 text-text-soft">{{ text }}</span>
</div>
</template>
<script setup lang="ts">
interface Props {
size?: 'sm' | 'md' | 'lg'
text?: string
containerClass?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 'md'
})
const spinnerClass = computed(() => {
const sizes = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8'
}
return sizes[props.size]
})
</script>
composables/useAuth.ts
export const useAuth = () => {
const user = ref(null)
const isAuthenticated = computed(() => !!user.value)
const login = async (credentials: { email: string; password: string }) => {
try {
// Simulate API call
const { data } = await $fetch('/api/auth/login', {
method: 'POST',
body: credentials
})
user.value = data.user
// Store token in httpOnly cookie
const token = useCookie('auth-token', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7 // 7 days
})
token.value = data.token
await navigateTo('/')
} catch (error) {
throw error
}
}
const register = async (userData: {
name: string
email: string
password: string
}) => {
try {
const { data } = await $fetch('/api/auth/register', {
method: 'POST',
body: userData
})
user.value = data.user
const token = useCookie('auth-token')
token.value = data.token
await navigateTo('/')
} catch (error) {
throw error
}
}
const logout = async () => {
try {
await $fetch('/api/auth/logout', { method: 'POST' })
} catch (error) {
// Handle error silently
} finally {
user.value = null
const token = useCookie('auth-token')
token.value = null
await navigateTo('/auth/login')
}
}
const fetchUser = async () => {
try {
const { data } = await $fetch('/api/auth/me')
user.value = data.user
} catch (error) {
// User not authenticated
user.value = null
}
}
return {
user: readonly(user),
isAuthenticated,
login,
register,
logout,
fetchUser
}
}
composables/useApi.ts
import type { UseFetchOptions } from 'nuxt/app'
export const useApi = <T>(
url: string | (() => string),
options: UseFetchOptions<T> = {}
) => {
return useFetch(url, {
...options,
$fetch: useNuxtApp().$fetch
})
}
export const useApiLazy = <T>(
url: string | (() => string),
options: UseFetchOptions<T> = {}
) => {
return useLazyFetch(url, {
...options,
$fetch: useNuxtApp().$fetch
})
}
middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (!isAuthenticated.value) {
return navigateTo('/auth/login')
}
})
middleware/guest.ts
export default defineNuxtRouteMiddleware((to, from) => {
const { isAuthenticated } = useAuth()
if (isAuthenticated.value) {
return navigateTo('/')
}
})
plugins/auth.client.ts
export default defineNuxtPlugin(async () => {
const { fetchUser } = useAuth()
// Fetch user on app initialization
await fetchUser()
})
server/api/auth/login.post.ts
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
export default defineEventHandler(async (event) => {
const { email, password } = await readBody(event)
// Validate input
if (!email || !password) {
throw createError({
statusCode: 400,
statusMessage: 'Email and password are required'
})
}
// In a real app, fetch user from database
const users = [
{
id: 1,
name: 'John Doe',
email: 'john@example.com',
password: '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi' // password
}
]
const user = users.find(u => u.email === email)
if (!user || !await bcrypt.compare(password, user.password)) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid credentials'
})
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' }
)
// Set httpOnly cookie
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7
})
return {
data: {
user: {
id: user.id,
name: user.name,
email: user.email
},
token
}
}
})
server/api/auth/register.post.ts
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
export default defineEventHandler(async (event) => {
const { name, email, password } = await readBody(event)
// Validate input
if (!name || !email || !password) {
throw createError({
statusCode: 400,
statusMessage: 'Name, email and password are required'
})
}
if (password.length < 6) {
throw createError({
statusCode: 400,
statusMessage: 'Password must be at least 6 characters'
})
}
// In a real app, check if user exists and save to database
const hashedPassword = await bcrypt.hash(password, 10)
const user = {
id: Math.floor(Math.random() * 1000) + 1,
name,
email,
password: hashedPassword
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '7d' }
)
// Set httpOnly cookie
setCookie(event, 'auth-token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7
})
return {
data: {
user: {
id: user.id,
name: user.name,
email: user.email
},
token
}
}
})
server/api/auth/logout.post.ts
export default defineEventHandler(async (event) => {
// Clear the auth cookie
setCookie(event, 'auth-token', '', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 0
})
return {
data: {
message: 'Logged out successfully'
}
}
})
server/api/auth/me.get.ts
import jwt from 'jsonwebtoken'
export default defineEventHandler(async (event) => {
const token = getCookie(event, 'auth-token')
if (!token) {
throw createError({
statusCode: 401,
statusMessage: 'No token provided'
})
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') as { userId: number }
// In a real app, fetch user from database
const user = {
id: decoded.userId,
name: 'John Doe',
email: 'john@example.com'
}
return {
data: {
user
}
}
} catch (error) {
throw createError({
statusCode: 401,
statusMessage: 'Invalid token'
})
}
})
tsconfig.json
{
"extends": "./.nuxt/tsconfig.json"
}
````md [README.md]
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
Start the development server on http://localhost:3000:
# npm
npm run dev
# pnpm
pnpm run dev
# yarn
yarn dev
# bun
bun run dev
Build the application for production:
# npm
npm run build
# pnpm
pnpm run build
# yarn
yarn build
# bun
bun run build
Locally preview production build:
# npm
npm run preview
# pnpm
pnpm run preview
# yarn
yarn preview
# bun
bun run preview
Check out the deployment documentation for more information.
- Clone and Install:
git clone <your-repo-url>
cd nuxt3-full-stack-app
npm install
cp .env.example .env
- Configure Environment:
Edit
.envfile with your values:
JWT_SECRET=your-super-secret-jwt-key
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
- Start Development Server:
npm run dev
# Docker deployment
docker-compose up -d
# Traditional deployment
npm run build
npm start
- ✅ Full-Stack: Complete Nuxt 3 application with server-side API
- ✅ Authentication: JWT-based auth with login/register/logout
- ✅ Database Ready: PostgreSQL integration with user management
- ✅ Modern UI: Tailwind CSS with dark mode support
- ✅ Internationalization: Multi-language support (EN, ES)
- ✅ TypeScript: Full type safety throughout the application
- ✅ Testing: Vitest setup with component and API tests
- ✅ Production Ready: Docker containers and deployment configs
- ✅ SEO Optimized: Meta tags, structured data, and performance
- ✅ Responsive: Mobile-first design with modern UI components
This Nuxt 3 template provides a comprehensive foundation for building modern, full-stack web applications with authentication, internationalization, and production-ready deployment configurations.
```json [package.json]
{
"name": "nuxt3-full-stack-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"typecheck": "nuxt typecheck",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
},
"dependencies": {
"@nuxt/ui": "^2.18.7",
"@nuxtjs/i18n": "^8.8.0",
"@nuxtjs/color-mode": "^3.5.1",
"@pinia/nuxt": "^0.5.5",
"@vueuse/nuxt": "^11.3.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"nuxt": "^3.15.1",
"pinia": "^2.2.8",
"zod": "^3.24.1"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.18",
"@iconify-json/simple-icons": "^1.2.18",
"@nuxt/devtools": "^1.7.4",
"@nuxt/eslint": "^0.8.0",
"@nuxt/test-utils": "^3.15.1",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.7",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.17.0",
"happy-dom": "^15.11.6",
"playwright-core": "^1.49.0",
"sass": "^1.82.0",
"typescript": "^5.7.2",
"vitest": "^2.1.8",
"vue-tsc": "^2.2.0"
}
}
```
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@nuxtjs/i18n',
'@nuxtjs/color-mode',
'@pinia/nuxt',
'@vueuse/nuxt',
'@nuxt/devtools',
'@nuxt/eslint'
],
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
typescript: {
strict: true,
typeCheck: true
},
runtimeConfig: {
jwtSecret: process.env.JWT_SECRET || 'your-secret-key',
databaseUrl: process.env.DATABASE_URL,
public: {
apiBase: process.env.API_BASE_URL || '/api',
appName: 'Nuxt 3 App',
appVersion: '1.0.0'
}
},
app: {
head: {
charset: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
title: 'Nuxt 3 Full-Stack App',
meta: [
{ name: 'description', content: 'A modern, full-stack Nuxt 3 application' },
{ name: 'format-detection', content: 'telephone=no' }
]
}
},
ui: {
global: true,
icons: ['lucide', 'simple-icons']
},
i18n: {
locales: [
{ code: 'en', name: 'English', file: 'en.json' },
{ code: 'es', name: 'Español', file: 'es.json' }
],
defaultLocale: 'en',
langDir: 'locales/',
strategy: 'prefix_except_default'
}
})
```
```bash [Dockerfile]
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nuxtjs
COPY --from=builder --chown=nuxtjs:nodejs /app/.output /app/.output
USER nuxtjs
EXPOSE 3000
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000
ENV NODE_ENV=production
CMD ["node", ".output/server/index.mjs"]
```
```yaml [docker-compose.yml]
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- JWT_SECRET=your-super-secret-jwt-key
- DATABASE_URL=postgresql://nuxt_user:nuxt_pass@postgres:5432/nuxt_db
depends_on:
- postgres
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=nuxt_user
- POSTGRES_PASSWORD=nuxt_pass
- POSTGRES_DB=nuxt_db
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
postgres_data:
```
::
Next
React
Want to update?
Subscribe to my blog to receive the latest updates from us.
Monthly articles
New articles will be created every month. No spam
All notifications are notified to you, not spam or advertising.