How Cool Is Nuxt? Building and Deploying Your First Full‑Stack App in Minutes
There’s something truly satisfying about creating a project that goes from your editor to the web in just a few clicks. With Nuxt 3, that experience feels almost magical — easy, free, and incredibly empowering. You can spin up a frontend app, add a small API, push it to GitHub, and deploy it on Vercel before your coffee even gets cold.
Let’s talk about why Nuxt makes that so fun, and how it’s not just a frontend framework — it’s a full‑stack toolkit that takes care of almost everything for you.
The magic of Nuxt 3
Nuxt has always been about simplicity, but Nuxt 3 takes it to another level. You start with a single command:
npx nuxi init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev
And boom — you’re looking at a live app running locally, powered by Vue 3, TypeScript, and server-side rendering by default.
The best part? It’s all free. No licenses, no hidden fees. You can host your app anywhere, but pairing it with Vercel gives you automatic deployments, previews for every branch, and a production‑ready environment with zero setup.
A small example: the TODO app
Imagine we’re building something simple: a TODO app. You can add tasks, mark them done, and store them in a small database (like Supabase). We’ll keep everything in one Nuxt project — frontend and backend together.
Nuxt handles the frontend using Vue 3’s composition API, so you can write components that are simple and reactive. And for the backend? That’s where Nitro, Nuxt’s built‑in server engine, comes in. It lets you write API endpoints right next to your app code, without needing a separate backend.
Here’s how it looks in practice.
The frontend
Our main page could look like this:
<template>
<main class="container">
<h1>Nuxt TODOs</h1>
<form @submit.prevent="createTodo">
<input v-model="newText" placeholder="What to do?" />
<button type="submit">Add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
<label>
<input type="checkbox" :checked="todo.done" @change="toggle(todo)" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</label>
</li>
</ul>
</main>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useTodos } from '~/composables/useTodos';
const { todos, fetchTodos, addTodo, updateTodo } = useTodos();
const newText = ref('');
onMounted(() => fetchTodos());
async function createTodo() {
if (!newText.value.trim()) return;
await addTodo({ text: newText.value.trim() });
newText.value = '';
}
async function toggle(todo: any) {
await updateTodo({ ...todo, done: !todo.done });
}
</script>
<style scoped>
.container {
max-width: 680px;
margin: 40px auto;
}
.done {
text-decoration: line-through;
}
</style>
This simple page uses a composable, a Nuxt concept that lets you group and reuse logic easily.
The composable: useTodos.ts
import { ref } from 'vue';
import { useFetch } from '#app';
export function useTodos() {
const todos = ref<any[]>([]);
async function fetchTodos() {
const { data } = await useFetch('/api/todos');
todos.value = data.value ?? [];
}
async function addTodo(payload: { text: string }) {
const { data } = await useFetch('/api/todos', {
method: 'POST',
body: payload,
});
todos.value.push(data.value);
}
async function updateTodo(payload: any) {
const { data } = await useFetch('/api/todos', {
method: 'POST',
body: payload,
});
todos.value = todos.value.map((t) =>
t.id === data.value.id ? data.value : t,
);
}
return { todos, fetchTodos, addTodo, updateTodo };
}
So far, all we did was call /api/todos. But where does that come from? That’s where Nitro steps in.
The backend, right inside your app
Nuxt’s Nitro server lets you create small endpoints without any configuration. You just drop a file into server/api/ and it’s automatically exposed as an API route.
For this example I've used Supabase as database, it provides different services (like Authentication, Edge Functions, Database) and it has a very good free plan to start you projects and test it out. Another usefull thing about supabase is the cli. It offers various tools, but the one that are the most usefull are: running the entire infrastructure locally using docker and generate types using the database schema (if you are using typescript in your project is pretty usefull).
// server/api/todos.get.ts
import { getSupabaseClient } from '../db/supabase.server';
export default defineEventHandler(async () => {
const supabase = getSupabaseClient();
const { data, error } = await supabase
.from('todos')
.select('*')
.order('id', { ascending: true });
if (error)
throw createError({ statusCode: 500, statusMessage: error.message });
return data;
});
That’s it. No Express server, no routing setup. And you can just as easily add a POST handler:
// server/api/todos.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const supabase = getSupabaseClient();
const { data, error } = await supabase
.from('todos')
.insert({ text: body.text, done: false })
.select()
.single();
if (error)
throw createError({ statusCode: 500, statusMessage: error.message });
return data;
});
Everything lives together in one repo, and Nitro handles the server logic automatically — locally or when deployed.
From code to production: GitHub and Vercel
Now for the fun part: putting this thing online.
Push your project to GitHub:
git init
git add .
git commit -m "feat: initial todo app"
gh repo create my-nuxt-todos --public --source=. --push
From there, open vercel.com, import your repository, and click Deploy. That’s literally it. Vercel detects Nuxt automatically, installs dependencies, builds the project, and deploys it.
Every time you open a pull request, Vercel creates a preview deployment so you can see your changes live before merging. When you merge to main, it redeploys automatically.
It’s continuous delivery without a single line of YAML — though, of course, you can still add your own CI if you want tests, linting, or version bumping.
Going a bit further: commits, changelogs, and CI/CD
If you want your project to feel more professional, add a few developer-friendly touches:
- Use commitlint with Conventional Commits (
feat:,fix:,chore:…) so your history stays clean. - Use standard-version or semantic-release to automatically generate changelogs and bump versions.
- Create a feature branch per pull request — Vercel will handle previews for each branch automatically.
Here’s what a small workflow could look like:
- You create
feature/add-todo-priority. - Commit with
feat: add priority flag to todos. - Push, open a PR, and Vercel gives you a live preview link.
- Merge — CI updates the changelog, tags a new version, and Vercel redeploys production.
And that’s it: a modern full‑stack workflow, all from one framework and one click‑deploy platform.
Why this matters
Nuxt isn’t just about rendering pages. It’s about giving developers a single, elegant place to build everything — from composables to backend APIs. With Nitro, Vercel, and tools like Supabase, you can go from idea to production in an afternoon.
You don’t need a DevOps setup. You don’t need to manage servers. You just write code.
And the best part? It’s still just Vue under the hood — easy to read, easy to write, and endlessly flexible.
So next time you have a small idea, don’t overthink it. Run npx nuxi init, push to GitHub, deploy to Vercel, and let Nuxt handle the rest.
TL;DR: Nuxt + Nitro + Vercel = full‑stack freedom. One repo, one framework, infinite possibilities.