Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Получение данных в Nuxt 3
nuxt_data_fetching

Получение данных в Nuxt 3

useFetch, useAsyncData, $fetch, серверные маршруты, defineEventHandler, обработка ошибок и кэширование

Получение данных в Nuxt 3

Nuxt 3 предоставляет специальные composables для получения данных, которые работают как на сервере, так и на клиенте. Это решает ключевую проблему SSR: данные должны быть загружены до отправки HTML клиенту.

#Проблема data fetching в SSR контексте

В обычном Vue SPA загрузка данных простая: компонент монтируется, вызывается onMounted, делается fetch, данные отображаются. В SSR всё сложнее.

При серверном рендеринге сервер должен: выполнить код компонента, загрузить все данные, отрендерить HTML и только потом отправить его клиенту. Проблема: onMounted не выполняется на сервере (там нет DOM). Обычный fetch в onMounted означает, что сервер отдаст пустую страницу, а данные загрузятся уже в браузере — это убивает смысл SSR.

Nuxt решает это через специальные composables useFetch и useAsyncData, которые выполняются как на сервере (до рендеринга), так и на клиенте (при навигации).

#$fetch — низкоуровневый HTTP клиент

$fetch — это ofetch, мощный fetch-клиент с автоматическим парсингом JSON, обработкой ошибок и поддержкой TypeScript. Он авто-импортируется в Nuxt:

<script setup> // $fetch — прямой запрос, без кэширования и SSR-оптимизации // Используйте для POST/PUT/DELETE или в обработчиках событий async function createPost(data) { try { const post = await $fetch('/api/posts', { method: 'POST', body: data, headers: { 'Authorization': `Bearer ${useAuthStore().token}` } }) console.log('Создан пост:', post) } catch (error) { // ofetch автоматически бросает FetchError при non-2xx статусах console.error('Статус:', error.status, 'Данные:', error.data) } } // $fetch с query params const users = await $fetch('/api/users', { query: { page: 1, limit: 10, role: 'admin' } }) // $fetch с кастомным ответом const response = await $fetch.raw('/api/upload', { method: 'POST', body: formData }) console.log(response.status, response.headers.get('x-request-id')) </script>

Важно: $fetch в <script setup> на сервере будет выполнен при каждом запросе. В компоненте SSR это нормально, но для данных, нужных при начальном рендере, лучше использовать useFetch или useAsyncData.

#useFetch — composable с SSR поддержкой

useFetch — основной способ получения данных в Nuxt. Он автоматически:

  • Выполняет запрос на сервере при первом рендере
  • Передаёт данные клиенту (гидратация), избегая дублирования запроса
  • При клиентской навигации выполняет запрос в браузере
  • Предоставляет реактивные data, pending, error
<template> <div> <div v-if="pending">Загрузка постов...</div> <div v-else-if="error"> <p>Ошибка: {{ error.message }}</p> <button @click="refresh">Повторить</button> </div> <ul v-else> <li v-for="post in posts" :key="post.id"> <h3>{{ post.title }}</h3> <p>{{ post.body }}</p> </li> </ul> </div> </template> <script setup> // useFetch автоматически определяет ключ кэша по URL const { data: posts, pending, error, refresh } = await useFetch('/api/posts') </script>

Обратите внимание на await перед useFetch. Это не блокирует рендеринг — Nuxt использует <Suspense> под капотом. Страница рендерится только после загрузки данных.

useFetch с параметрами и опциями:

<script setup> const route = useRoute() const page = ref(1) const limit = ref(10) // Динамические параметры — запрос повторяется при их изменении const { data, pending, error, refresh } = await useFetch('/api/posts', { // query параметры — реактивные, запрос обновится при изменении query: { page, limit, category: route.query.category }, // Ключ кэша — по умолчанию URL + query, можно указать явно key: `posts-page-${page.value}`, // Преобразование данных transform: (response) => ({ posts: response.data, total: response.meta.total }), // Выполнять только на клиенте server: false, // Ленивая загрузка — не блокирует рендеринг страницы lazy: true, // Заголовки запроса headers: useRequestHeaders(['cookie']), // Обработчик ошибок onResponseError({ response }) { if (response.status === 401) { navigateTo('/login') } } }) </script>

#useAsyncData — гибкий вариант

useAsyncData более гибкий, чем useFetch. Он принимает функцию вместо URL, что позволяет использовать любой источник данных:

<script setup> // useAsyncData(key, fetchFunction, options) // key — уникальный ключ для кэширования и дедупликации const { data: user, pending, error } = await useAsyncData( 'current-user', () => $fetch('/api/auth/me') ) // Несколько источников данных в одном запросе const { data } = await useAsyncData('dashboard', async () => { const [stats, recentPosts, notifications] = await Promise.all([ $fetch('/api/stats'), $fetch('/api/posts?limit=5'), $fetch('/api/notifications?unread=true') ]) return { stats, recentPosts, notifications } }) // Доступ к данным if (data.value) { console.log(data.value.stats) console.log(data.value.recentPosts) } </script>

useAsyncData с зависимостями и ручным обновлением:

<script setup> const userId = ref(null) const authStore = useAuthStore() // Запрос зависит от userId — перезапустится при изменении const { data: profile, refresh: refreshProfile } = await useAsyncData( () => `user-profile-${userId.value}`, // Динамический ключ () => { if (!userId.value) return null return $fetch(`/api/users/${userId.value}/profile`) }, { watch: [userId], // Перезапускать при изменении immediate: true, // Выполнить сразу default: () => null // Значение по умолчанию до загрузки } ) // Принудительное обновление async function updateProfile(data) { await $fetch(`/api/users/${userId.value}/profile`, { method: 'PUT', body: data }) await refreshProfile() } </script>

#Разница между useFetch и useAsyncData

Понимание разницы помогает выбрать правильный инструмент. По сути useFetch — это синтаксический сахар над useAsyncData + $fetch:

// useFetch('/api/posts') — это то же самое что: useAsyncData('get-/api/posts', () => $fetch('/api/posts'))

Когда использовать useFetch:

  • Простые GET-запросы к API по URL
  • Когда ключ кэша можно вычислить из URL
  • Быстрый и лаконичный код

Когда использовать useAsyncData:

  • Несколько параллельных запросов (Promise.all)
  • Данные из нескольких источников (не только HTTP)
  • Нужен полный контроль над ключом кэша
  • Данные из хранилища или других composables

#Server Routes — встроенный API

Nuxt позволяет создавать API endpoints прямо в проекте через папку server/api/. Они работают на сервере и не попадают в клиентский бандл:

// server/api/hello.ts // Доступен как GET /api/hello export default defineEventHandler((event) => { return { message: 'Привет от сервера!', time: new Date().toISOString() } })

Роутинг серверных маршрутов основан на именовании файлов:

server/api/
├── hello.ts              → GET /api/hello
├── users/
│   ├── index.ts          → GET /api/users
│   ├── index.post.ts     → POST /api/users  (суффикс метода!)
│   ├── [id].ts           → GET /api/users/:id
│   ├── [id].put.ts       → PUT /api/users/:id
│   └── [id].delete.ts    → DELETE /api/users/:id
└── auth/
    ├── login.post.ts     → POST /api/auth/login
    └── logout.post.ts    → POST /api/auth/logout

Более сложный обработчик с параметрами и телом запроса:

// server/api/users/[id].ts import { defineEventHandler, getRouterParam, createError } from 'h3' export default defineEventHandler(async (event) => { // Параметры маршрута const id = getRouterParam(event, 'id') if (!id || isNaN(Number(id))) { throw createError({ statusCode: 400, statusMessage: 'Некорректный ID пользователя' }) } // Запрос к базе данных (в реальном проекте) const user = await db.users.findById(Number(id)) if (!user) { throw createError({ statusCode: 404, statusMessage: `Пользователь с ID ${id} не найден` }) } return user })
// server/api/users/index.get.ts import { defineEventHandler, getQuery } from 'h3' export default defineEventHandler(async (event) => { // Query параметры: /api/users?page=1&limit=10 const query = getQuery(event) const page = Number(query.page) || 1 const limit = Number(query.limit) || 10 const users = await db.users.findAll({ offset: (page - 1) * limit, limit, orderBy: 'createdAt' }) return { data: users, meta: { page, limit, total: await db.users.count() } } })
// server/api/posts/index.post.ts import { defineEventHandler, readBody, createError } from 'h3' export default defineEventHandler(async (event) => { // Тело запроса const body = await readBody(event) // Валидация if (!body.title || body.title.length < 3) { throw createError({ statusCode: 422, statusMessage: 'Заголовок должен содержать минимум 3 символа' }) } const post = await db.posts.create({ title: body.title, content: body.content, authorId: event.context.auth?.userId // Из middleware аутентификации }) // Устанавливаем статус ответа setResponseStatus(event, 201) return post })

#Обработка ошибок в server routes

createError создаёт H3Error с нужным статусом. На клиенте эта ошибка попадает в error из useFetch:

// server/api/protected.ts export default defineEventHandler(async (event) => { const token = getCookie(event, 'access_token') if (!token) { throw createError({ statusCode: 401, statusMessage: 'Не авторизован', data: { code: 'UNAUTHORIZED' } }) } try { const decoded = verifyJWT(token) return { userId: decoded.sub, data: 'Секретные данные' } } catch { throw createError({ statusCode: 403, statusMessage: 'Токен недействителен' }) } })
<!-- Клиент обрабатывает ошибку --> <script setup> const { data, error } = await useFetch('/api/protected') // Обработка ошибок разных типов if (error.value) { console.log(error.value.statusCode) // 401 console.log(error.value.statusMessage) // Не авторизован console.log(error.value.data) // { code: 'UNAUTHORIZED' } } </script>

#Кэширование с cachedEventHandler

Для серверных маршрутов с тяжёлыми вычислениями или внешними запросами можно включить кэширование:

// server/api/stats.ts import { cachedEventHandler } from 'nitro/runtime' export default cachedEventHandler(async (event) => { // Этот код будет выполнен только раз за 60 секунд // Потом Nitro отдаёт закэшированный ответ const stats = await computeHeavyStats() return stats }, { maxAge: 60, // Кэш на 60 секунд swr: true, // Stale-while-revalidate: отдавать старое пока обновляется name: 'stats-cache', getKey: (event) => `stats-${getQuery(event).period || 'day'}` })

#useLazyFetch и useLazyAsyncData

Ленивые варианты не блокируют рендеринг страницы. Страница показывается сразу, данные догружаются:

<template> <div> <!-- Страница рендерится сразу, данные подгружаются --> <div v-if="pending">Загружаем комментарии...</div> <CommentList v-else :comments="comments" /> </div> </template> <script setup> // useLazyFetch — не блокирует рендеринг // pending = true изначально, страница показывается без данных const { data: comments, pending } = useLazyFetch('/api/comments') // Нет await — не блокируем! </script>

Далее: Аутентификация в Nuxt 3