All posts

How Cool Is Nuxt?

Building and Deploying Your First Full-Stack App in Minutes

There's something satisfying about taking a project from your text editor all the way to a live website with a couple of clicks. Nuxt 3 makes this feel almost magical, or maybe I'm just tired from too many coffees. Either way, you spin up a frontend app, throw in a tiny API endpoint, push to GitHub, and Vercel deploys it before your coffee cools down. It's addictive.

So why does this workflow work so well in Nuxt, and why isn't it just another frontend framework pretending to be full-stack?

The magic of Nuxt 3

Nuxt has always leaned into simplicity, and Nuxt 3 takes it further. Start with this:

npx nuxi init my-nuxt-app
cd my-nuxt-app
npm install
npm run dev

And boom: a live app running locally, with Vue 3, TypeScript, and server-side rendering from the get-go. It's free, with no licenses or hidden fees. You can host it on Vercel, Netlify, S3, or your own server. Vercel is especially good here: you get automatic deployments, preview builds for every PR branch and production-ready env vars with zero setup.

A small example: the TODO app

Let's build something simple: a TODO list where you add tasks, mark them done, and store them in a database. I'm using Supabase here, but any DB works. Frontend and backend live together in one Nuxt repo.

The frontend is Vue 3 with the Composition API. The backend is Nitro, Nuxt's built-in server engine, which gives you API endpoints right next to your app code with no separate backend server.

Here's what the frontend looks like:

<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 uses a composable, a Vue 3 concept that lets you group and reuse logic without bloat. Nuxt auto-imports anything in composables/.

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 };
}

All of these calls hit /api/todos, which comes from Nitro.

The backend, right inside your app

Nuxt's Nitro server lets you create endpoints without configuration. Drop a file into server/api/ and it automatically becomes an API route.

Using Supabase as the database, here's a GET handler:

// 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;
});

There's no Express and no routing setup. Adding a POST handler is just as easy:

// 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;
});

Nitro runs the server logic both locally and in production.

From code to production: GitHub and Vercel

Time to go live:

git init
git add .
git commit -m "feat: initial todo app"
gh repo create my-nuxt-todos --public --source=. --push

Open vercel.com, import your repo, and click Deploy. Vercel detects Nuxt, installs deps, builds and deploys. Every PR gets a preview deployment, and merging to main redeploys automatically.

That's continuous delivery without YAML, though you can still add CI for tests, linting and version bumps if you want.

Going a bit further: commits, changelogs, and CI/CD

A few extras make the project feel more professional:

  • Use commitlint with Conventional Commits (feat:, fix:, chore:) for a clean history.
  • Use standard-version or semantic-release to generate changelogs and bump versions automatically.
  • Create a feature branch per pull request. Vercel handles the previews.

Here's a small workflow:

  1. Create feature/add-todo-priority
  2. Commit with feat: add priority flag to todos
  3. Push, open a PR, Vercel gives you a live preview
  4. Merge. CI updates the changelog and tags a version, and Vercel redeploys production

That whole workflow comes from one framework and one deploy platform.

What you get out of it

Nuxt gives you a single place to build everything, from composables to backend APIs. With Nitro, Vercel, and tools like Supabase, you go from idea to production in an afternoon, with no DevOps setup and no servers to manage.

And it's still just Vue under the hood, so the code stays easy to read and change.

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 and Vercel give you a full-stack app in one repo with one framework, live in an afternoon.