Fullstack-приложение на Nuxt: от страниц до API

13 августа 2026
Автор

Антон Ларичев

Что такое Nuxt и зачем он нужен

Nuxt — это фреймворк поверх Vue.js, который превращает фронтенд-инструмент в полноценный fullstack-стек. Он даёт файловый роутинг, серверный рендеринг (SSR), статическую генерацию (SSG) и серверные API-роуты из коробки — всё в одном проекте, без настройки отдельного Express или Fastify.

Главное преимущество: один репозиторий, один npm run dev, и у вас работает и клиент, и сервер. Это особенно ценно для небольших продуктов, MVP и внутренних инструментов, где overhead отдельного бэкенда не оправдан.

В этой статье создадим реальное мини-приложение: список задач с хранением в памяти сервера, CRUD через API-роуты и реактивным UI на Vue-компонентах.

Курс по теме

Пройти курс Vue 3 и Pinia

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Инициализация проекта

Требования: Node.js 18+.

npx nuxi@latest init nuxt-todo
cd nuxt-todo
npm install
npm run dev

После запуска приложение доступно на http://localhost:3000. Структура проекта:

nuxt-todo/
├── app.vue          # корневой компонент
├── nuxt.config.ts   # конфигурация фреймворка
├── pages/           # файловый роутинг
├── components/      # переиспользуемые компоненты
├── server/
│   └── api/         # серверные API-роуты
└── composables/     # переиспользуемая логика

Файловый роутинг: страницы без конфигурации

Nuxt генерирует роуты автоматически на основе файловой структуры в папке pages/. Каждый файл — это маршрут.

pages/
├── index.vue          → /
├── about.vue          → /about
└── tasks/
    ├── index.vue      → /tasks
    └── [id].vue       → /tasks/:id

Чтобы роутинг заработал, нужно добавить <NuxtPage /> в корневой компонент. Обновим app.vue:

<template>
  <div>
    <header>
      <nav>
        <NuxtLink to="/">Главная</NuxtLink>
        <NuxtLink to="/tasks">Задачи</NuxtLink>
      </nav>
    </header>
    <main>
      <NuxtPage />
    </main>
  </div>
</template>

<NuxtLink> — это аналог <RouterLink> из Vue Router, но с дополнительными оптимизациями: предзагрузкой страниц при наведении и корректной работой с SSR.

Создадим главную страницу pages/index.vue:

<template>
  <div>
    <h1>Todo App на Nuxt</h1>
    <p>Простое fullstack-приложение с серверным API.</p>
    <NuxtLink to="/tasks">Перейти к задачам</NuxtLink>
  </div>
</template>

Динамические роуты

Файл pages/tasks/[id].vue создаёт динамический маршрут. Параметр id доступен через хук useRoute():

<script setup lang="ts">
const route = useRoute()
const taskId = computed(() => route.params.id as string)

const { data: task } = await useFetch(`/api/tasks/${taskId.value}`)
</script>

<template>
  <div v-if="task">
    <h2>{{ task.title }}</h2>
    <p>Статус: {{ task.done ? 'Выполнено' : 'В процессе' }}</p>
  </div>
  <div v-else>
    <p>Задача не найдена</p>
  </div>
</template>

Серверные API-роуты

Папка server/api/ — главная магия Nuxt для fullstack-разработки. Каждый файл здесь становится API-эндпоинтом, который выполняется только на сервере. Никакой секретный код или переменные среды не попадут в браузер.

Хранилище данных

Для примера используем хранение в памяти сервера. В реальном проекте здесь будет Prisma, Drizzle или любой другой ORM.

Создадим server/utils/db.ts:

export interface Task {
  id: number
  title: string
  done: boolean
  createdAt: string
}

let tasks: Task[] = [
  { id: 1, title: 'Изучить Nuxt', done: false, createdAt: new Date().toISOString() },
  { id: 2, title: 'Написать API', done: true, createdAt: new Date().toISOString() },
]

let nextId = 3

export function getTasks(): Task[] {
  return tasks
}

export function getTask(id: number): Task | undefined {
  return tasks.find(t => t.id === id)
}

export function createTask(title: string): Task {
  const task: Task = { id: nextId++, title, done: false, createdAt: new Date().toISOString() }
  tasks.push(task)
  return task
}

export function updateTask(id: number, data: Partial<Task>): Task | null {
  const index = tasks.findIndex(t => t.id === id)
  if (index === -1) return null
  tasks[index] = { ...tasks[index], ...data }
  return tasks[index]
}

export function deleteTask(id: number): boolean {
  const before = tasks.length
  tasks = tasks.filter(t => t.id !== id)
  return tasks.length < before
}

GET /api/tasks — список задач

// server/api/tasks/index.get.ts
import { getTasks } from '~/server/utils/db'

export default defineEventHandler(() => {
  return getTasks()
})

Суффикс .get.ts означает, что этот файл обрабатывает только GET-запросы. Nuxt поддерживает все HTTP-методы: .get.ts, .post.ts, .put.ts, .patch.ts, .delete.ts.

POST /api/tasks — создание задачи

// server/api/tasks/index.post.ts
import { createTask } from '~/server/utils/db'

export default defineEventHandler(async (event) => {
  const body = await readBody<{ title: string }>(event)

  if (!body.title?.trim()) {
    throw createError({ statusCode: 400, message: 'Поле title обязательно' })
  }

  const task = createTask(body.title.trim())
  setResponseStatus(event, 201)
  return task
})

readBody() автоматически парсит JSON из тела запроса. createError() возвращает корректный HTTP-ответ с нужным статусом.

GET /api/tasks/:id — одна задача

// server/api/tasks/[id].get.ts
import { getTask } from '~/server/utils/db'

export default defineEventHandler((event) => {
  const id = parseInt(getRouterParam(event, 'id') ?? '')

  if (isNaN(id)) {
    throw createError({ statusCode: 400, message: 'Некорректный id' })
  }

  const task = getTask(id)

  if (!task) {
    throw createError({ statusCode: 404, message: 'Задача не найдена' })
  }

  return task
})

PATCH /api/tasks/:id — обновление

// server/api/tasks/[id].patch.ts
import { updateTask } from '~/server/utils/db'

export default defineEventHandler(async (event) => {
  const id = parseInt(getRouterParam(event, 'id') ?? '')
  const body = await readBody<{ title?: string; done?: boolean }>(event)

  if (isNaN(id)) {
    throw createError({ statusCode: 400, message: 'Некорректный id' })
  }

  const task = updateTask(id, body)

  if (!task) {
    throw createError({ statusCode: 404, message: 'Задача не найдена' })
  }

  return task
})

DELETE /api/tasks/:id — удаление

// server/api/tasks/[id].delete.ts
import { deleteTask } from '~/server/utils/db'

export default defineEventHandler((event) => {
  const id = parseInt(getRouterParam(event, 'id') ?? '')

  if (isNaN(id)) {
    throw createError({ statusCode: 400, message: 'Некорректный id' })
  }

  const deleted = deleteTask(id)

  if (!deleted) {
    throw createError({ statusCode: 404, message: 'Задача не найдена' })
  }

  return { success: true }
})

Получение данных на клиенте

Nuxt предоставляет три composable для работы с данными: useFetch, useAsyncData и $fetch.

  • useFetch — основной инструмент. Работает и на сервере (SSR), и на клиенте. Кэширует результат, дедуплицирует запросы.
  • useAsyncData — более гибкий вариант, когда нужна произвольная асинхронная логика, не обязательно HTTP.
  • $fetch — низкоуровневый HTTP-клиент (аналог fetch), используется для мутаций.

Страница со списком задач

<!-- pages/tasks/index.vue -->
<script setup lang="ts">
import type { Task } from '~/server/utils/db'

const { data: tasks, refresh } = await useFetch<Task[]>('/api/tasks')

const newTitle = ref('')
const isAdding = ref(false)

async function addTask() {
  if (!newTitle.value.trim()) return
  isAdding.value = true

  try {
    await $fetch('/api/tasks', {
      method: 'POST',
      body: { title: newTitle.value },
    })
    newTitle.value = ''
    await refresh()
  } finally {
    isAdding.value = false
  }
}

async function toggleDone(task: Task) {
  await $fetch(`/api/tasks/${task.id}`, {
    method: 'PATCH',
    body: { done: !task.done },
  })
  await refresh()
}

async function removeTask(id: number) {
  await $fetch(`/api/tasks/${id}`, { method: 'DELETE' })
  await refresh()
}
</script>

<template>
  <div>
    <h1>Задачи</h1>

    <form @submit.prevent="addTask">
      <input
        v-model="newTitle"
        placeholder="Новая задача..."
        :disabled="isAdding"
      />
      <button type="submit" :disabled="isAdding">Добавить</button>
    </form>

    <ul>
      <li v-for="task in tasks" :key="task.id">
        <span :style="{ textDecoration: task.done ? 'line-through' : 'none' }">
          {{ task.title }}
        </span>
        <button @click="toggleDone(task)">
          {{ task.done ? 'Вернуть' : 'Выполнить' }}
        </button>
        <NuxtLink :to="`/tasks/${task.id}`">Детали</NuxtLink>
        <button @click="removeTask(task.id)">Удалить</button>
      </li>
    </ul>
  </div>
</template>

refresh() повторно запрашивает данные без перезагрузки страницы. При SSR useFetch выполняется на сервере, HTML приходит уже с данными — это важно для SEO и скорости.

Middleware: защита маршрутов

Nuxt middleware — это функции, которые выполняются перед переходом на страницу. Создадим простой пример защиты роута.

// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
  const isLoggedIn = useCookie('auth_token').value

  if (!isLoggedIn && to.path !== '/login') {
    return navigateTo('/login')
  }
})

Подключение middleware к конкретной странице:

<!-- pages/tasks/index.vue -->
<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

Composables: переиспользуемая логика

Папка composables/ автоматически импортируется во всех компонентах и страницах. Вынесем логику работы с задачами:

// composables/useTasks.ts
import type { Task } from '~/server/utils/db'

export function useTasks() {
  const { data: tasks, refresh, pending } = useFetch<Task[]>('/api/tasks')

  async function add(title: string) {
    await $fetch('/api/tasks', { method: 'POST', body: { title } })
    await refresh()
  }

  async function toggle(task: Task) {
    await $fetch(`/api/tasks/${task.id}`, {
      method: 'PATCH',
      body: { done: !task.done },
    })
    await refresh()
  }

  async function remove(id: number) {
    await $fetch(`/api/tasks/${id}`, { method: 'DELETE' })
    await refresh()
  }

  return { tasks, pending, add, toggle, remove }
}

Теперь любой компонент может использовать const { tasks, add, toggle, remove } = useTasks() без дублирования кода.

SEO и мета-теги

Nuxt предоставляет useHead() для управления метаданными страницы:

<script setup lang="ts">
useHead({
  title: 'Список задач | Todo App',
  meta: [
    { name: 'description', content: 'Управляйте своими задачами с помощью Todo App на Nuxt' }
  ]
})
</script>

Для динамических страниц (например, детальная страница задачи), мета-теги можно генерировать на основе загруженных данных:

<script setup lang="ts">
const route = useRoute()
const { data: task } = await useFetch(`/api/tasks/${route.params.id}`)

useHead(() => ({
  title: task.value ? `${task.value.title} | Todo App` : 'Задача не найдена'
}))
</script>

Конфигурация и переменные среды

nuxt.config.ts — центральная точка настройки. Переменные среды доступны через runtimeConfig:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Доступны ТОЛЬКО на сервере
    databaseUrl: process.env.DATABASE_URL,
    secretKey: process.env.SECRET_KEY,
    // Доступны на клиенте и сервере
    public: {
      apiBase: process.env.NUXT_PUBLIC_API_BASE ?? '/api',
    }
  }
})

Использование в серверном коде:

// server/api/tasks/index.get.ts
export default defineEventHandler((event) => {
  const config = useRuntimeConfig(event)
  console.log('DB URL:', config.databaseUrl) // только на сервере
  return getTasks()
})

В клиентском компоненте:

<script setup lang="ts">
const config = useRuntimeConfig()
console.log('API base:', config.public.apiBase) // /api
</script>

Итог

Nuxt позволяет строить fullstack-приложения в едином стеке:

  • pages/ — файловый роутинг без конфигурации, динамические сегменты через [param]
  • server/api/ — HTTP-роуты, разделённые по методам суффиксами .get.ts, .post.ts и т.д.
  • useFetch — получение данных с SSR-поддержкой и автоматическим кэшированием
  • $fetch — HTTP-клиент для мутаций
  • composables/ — автоимпортируемая переиспользуемая логика
  • middleware/ — хуки навигации для защиты роутов и логирования
  • runtimeConfig — типобезопасные переменные среды с разделением сервер/клиент

Эта архитектура покрывает большинство задач: от прототипа до продакшн-приложения с аутентификацией, базой данных и SEO.

Для глубокого изучения Vue.js и экосистемы вокруг него, включая Nuxt, рекомендуем курс на PurpleSchool: Курс по Vue.js

Стрелочка влевоProps компонента в React - полный разбор с примерамиФункциональные компоненты в React - функциональный подход к построению интерфейсовСтрелочка вправо

Постройте личный план изучения Vue до уровня Middle — бесплатно!

Vue — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Vuex - полное руководство по управлению состоянием во Vue приложенияхРеактивные ссылки ref - полный разбор для разработчиковРеактивные объекты reactive-objects - подробное руководство с примерамиРеактивные переменные - концепция reactive и практические примерыМеханизм Provide Inject - как он работает и когда применятьPinia современный менеджер состояния для VueЛокальное состояние local state в веб разработкеГлобальное состояние в приложениях - global state
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
Функция append в Go GolangОтображение компонента mounted - практическое руководствоХуки жизненного цикла компонентов - полное руководство для разработчиковУничтожение компонента destroyed - как правильно очищать ресурсы и подпискиИнициализация данных в состоянии created - как и когда подготавливать данные в приложенииОбновление компонента beforeUpdate во VueМонтирование компонента - хук beforeMount в VueРазрушение компонента во Vue - beforeDestroy и beforeUnmountСоздание экземпляра beforeCreate - полный разбор жизненного цикла
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Слоты компонента - концепция и практическое использованиеРегистрация компонентов component-registration в приложениях с внедрением зависимостейProps компонента в React - полный разбор с примерамиFullstack-приложение на Nuxt: от страниц до APIФункциональные компоненты в React - функциональный подход к построению интерфейсовСобытия компонента - events в современных интерфейсахДинамические компоненты - dynamic-componentsСоздание компонента component - практическое руководствоАсинхронные компоненты async-components - практическое руководство
Наблюдатели watchers - от паттерна до практических реализацийУправление переменными и реактивными свойствами во VueИспользование v for и slot в VueПрименение v-bind для динамической привязки атрибутов в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsШаблоны Vue templates - практическое руководство для разработчиковИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueРеактивность Vue reactivity - как это работает под капотом и как этим пользоватьсяПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueОпции компонента в Go - паттерн component-optionsРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreВычисляемые свойства computed во Vue.jsСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueОсновы Vue - vue-basics для уверенного стартаИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueИспользование transition во VueТестирование компонентов и приложений на VueТелепортация - архитектура и реализация в серверных приложенияхРабота с teleport для управления DOM во VueSuspense в React - управление асинхронными данными и ленивой загрузкойПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиПлагины Vue vue-plugins - полное практическое руководствоРуководство по nextTick для работы с DOMМиксины - mixins в современном программированииJSX в Vue с использованием плагина vue-jsxСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьКастомные элементы - Custom Elements в современном JavaScriptИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Vue 3 и Pinia

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий