Dominando useRequestFetch: El Poder del Fetching en SSR
¿Qué es useRequestFetch?
useRequestFetch es un composable de Nitro (server-side) que proporciona una instancia de fetching con el contexto HTTP actual. Es la herramienta para hacer peticiones HTTP dentro de handlers, middleware o rutas API con las credenciales correctas del request.
Características clave:
- ✅ Solo funciona en servidor (runtime de Nitro)
- ✅ Preserva cookies y headers del request original
- ✅ Ideal para server-side calls entre APIs
- ✅ Manipulación del contexto HTTP
¿Cuándo usar useRequestFetch?
Úsalo cuando necesites hacer peticiones HTTP desde el servidor manteniendo el contexto del usuario:
Proxy de APIs: Hacer llamadas a APIs externas desde tu APIBFF (Backend For Frontend): Agregar datos de múltiples fuentesAutenticación: Mantener sesiones y cookiesRate limiting: Controlar peticiones desde el servidor
Sintaxis Básica
ts
// Only available in server-side contexts
const data = await useRequestFetch(event)(url, options);Parámetros:
event: El evento de Nitro (H3Event)url: URL a la que hacer fetchoptions: Opciones de fetch (igual que$fetch)
Ejemplo Práctico Básico
ts
// server/api/proxy/posts.get.ts
export default defineEventHandler(async (event) => {
// Use useRequestFetch to maintain cookies and headers
const posts = await useRequestFetch(event)(
'https://jsonplaceholder.typicode.com/posts'
);
return { posts };
});Diferencia Crítica: $fetch vs useRequestFetch
ts
// server/api/test.ts
export default defineEventHandler(async (event) => {
// ❌ $fetch does NOT preserve the request context
const with$fetch = await $fetch('https://api.example.com/data');
// Original headers are NOT sent
// ✅ useRequestFetch DOES preserve context
const withUseRequestFetch = await useRequestFetch(event)(
'https://api.example.com/data'
);
// Headers, cookies, etc. They are preserved
return { with$fetch, withUseRequestFetch };
});Casos de Uso Avanzados
Proxy con Autenticación
ts
// server/api/proxy/secure-data.get.ts
export default defineEventHandler(async (event) => {
// Verify authentication first
const session = await getServerSession(event);
if (!session) {
throw createError({ statusCode: 401, message: 'No autorizado' });
}
// Make a request to an external API with authentication headers
const secureData = await useRequestFetch(event)(
'https://api.segura.com/data',
{
headers: {
'X-Api-Key': process.env.EXTERNAL_API_KEY
}
}
);
return secureData;
});Agregación de Datos (BFF Pattern)
ts
// server/api/dashboard.get.ts
export default defineEventHandler(async (event) => {
const [posts, users, stats] = await Promise.all([
// They all maintain the context of the original request
useRequestFetch(event)('https://jsonplaceholder.typicode.com/posts'),
useRequestFetch(event)('https://jsonplaceholder.typicode.com/users'),
useRequestFetch(event)('/api/internal/stats') // API interna
]);
return {
posts: posts.slice(0, 5),
users: users.slice(0, 3),
stats
};
});Rate Limiting y Cache en Servidor
ts
// server/api/cached-posts.get.ts
import { createStorage } from 'unstorage';
const storage = createStorage();
export default defineEventHandler(async (event) => {
const cacheKey = 'posts-cache';
const cached = await storage.getItem(cacheKey);
if (cached) {
return cached;
}
// Fetch with rate limiting
const posts = await useRequestFetch(event)(
'https://jsonplaceholder.typicode.com/posts',
{
// Custom rate limiting
retry: 2,
retryDelay: 1000
}
);
// Caching for 5 minutes
await storage.setItem(cacheKey, posts, { ttl: 300000 });
return posts;
});Manipulación de Headers
ts
// server/api/proxy-with-headers.get.ts
export default defineEventHandler(async (event) => {
// Obtener headers del request original
const originalHeaders = getHeaders(event);
const response = await useRequestFetch(event)(
'https://api.externa.com/data',
{
headers: {
// Pass specific headers
'Authorization': originalHeaders.authorization,
'User-Agent': 'Nuxt-App/1.0',
// Delete or modify headers
'Cookie': undefined, // Do not pass cookies
'X-Custom-Header': 'value-modified'
}
}
);
return response;
});Manejo de Errores en Servidor
ts
// server/api/robust-proxy.get.ts
export default defineEventHandler(async (event) => {
try {
const data = await useRequestFetch(event)(
'https://jsonplaceholder.typicode.com/posts/9999'
);
return data;
} catch (error: any) {
// Detailed server log
console.error('Proxy error:', {
url: error.url,
status: error.statusCode,
message: error.message,
timestamp: new Date().toISOString()
});
// Controlled error response
throw createError({
statusCode: error.statusCode || 502,
message: `Service error: ${error.message}`
});
}
});Middleware con useRequestFetch
ts
// server/middleware/auth.ts
export default defineEventHandler(async (event) => {
// Verify token on each request
const authHeader = getHeader(event, 'authorization');
if (authHeader) {
try {
// Validate token with authentication service
const userData = await useRequestFetch(event)(
`${process.env.AUTH_SERVICE}/validate-token`,
{
headers: { authorization: authHeader }
}
);
// Add user data to context
event.context.user = userData;
} catch (error) {
console.warn('Invalid token:', error);
}
}
});Testing y Mocking
ts
// server/api/test-proxy.ts
import { defineEventHandler } from 'h3';
// Mock for development
const isDev = process.env.NODE_ENV === 'development';
export default defineEventHandler(async (event) => {
if (isDev) {
// Mock data for development
return {
mocked: true,
data: [
{ id: 1, name: 'Mock Item 1' },
{ id: 2, name: 'Mock Item 2' }
]
};
}
// Production: real call
const realData = await useRequestFetch(event)(
'https://api.real.com/data'
);
return realData;
});Configuración Global de Proxy
ts
// server/utils/proxy.ts
export const createProxy = (baseURL: string) => {
return defineEventHandler(async (event) => {
const path = getRouterParam(event, 'path');
const method = event.method;
// Rebuild destination URL
const targetURL = `${baseURL}/${path}`;
// Get body if it's POST/PUT
const body = method !== 'GET' && method !== 'HEAD'
? await readBody(event)
: undefined;
// Proxy request
return useRequestFetch(event)(targetURL, {
method,
body,
headers: getHeaders(event)
});
});
};
// Use on routes
// server/api/proxy/[...path].ts
export default createProxy('https://jsonplaceholder.typicode.com');Performance y Optimización
ts
// server/api/optimized-posts.get.ts
export default defineEventHandler(async (event) => {
// HTTP cache control
const cached = await getCached(event, 'posts');
if (cached) {
setHeader(event, 'X-Cache', 'HIT');
return cached;
}
// Optimized fetch
const startTime = Date.now();
const posts = await useRequestFetch(event)(
'https://jsonplaceholder.typicode.com/posts',
{
// Aggressive timeout
timeout: 3000,
// Only required fields
query: { _limit: '20', _fields: 'id,title,userId' }
}
);
const duration = Date.now() - startTime;
setHeader(event, 'X-Response-Time', `${duration}ms`);
// Caching response
await cacheResponse(event, 'posts', posts, 60000); // 1 minuto
setHeader(event, 'X-Cache', 'MISS');
return posts;
});Seguridad y Best Practices
ts
// server/api/secure-proxy.get.ts
export default defineEventHandler(async (event) => {
// Validate destination URL
const target = getQuery(event).url as string;
if (!target || !isAllowedDomain(target)) {
throw createError({
statusCode: 400,
message: 'URL not allowed'
});
}
// Limit response size
const maxSize = 1024 * 1024; // 1MB
const response = await useRequestFetch(event)(target, {
onResponse({ response }) {
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength) > maxSize) {
throw createError({
statusCode: 413,
message: 'Too big an answer'
});
}
}
});
// Sanitize response
return sanitizeResponse(response);
});Ejemplo Completo: API Gateway
ts
// server/api/gateway/[...path].ts
export default defineEventHandler(async (event) => {
const services = {
posts: 'https://jsonplaceholder.typicode.com',
users: 'https://reqres.in/api',
internal: process.env.INTERNAL_API_URL
};
const path = getRouterParam(event, 'path')?.split('/') || [];
const serviceName = path[0];
const servicePath = path.slice(1).join('/');
if (!services[serviceName]) {
throw createError({
statusCode: 404,
message: `Service ${serviceName} not found`
});
}
// Proxy to the corresponding service
const targetURL = `${services[serviceName]}/${servicePath}`;
const method = event.method;
const response = await useRequestFetch(event)(targetURL, {
method,
headers: {
// Pass necessary headers
'Authorization': getHeader(event, 'authorization'),
'Content-Type': getHeader(event, 'content-type'),
// Do not pass cookies between services
'Cookie': undefined
},
// Pass body to POST/PUT/PATCH
body: ['POST', 'PUT', 'PATCH'].includes(method)
? await readBody(event).catch(() => undefined)
: undefined
});
// Add response headers
setHeader(event, 'X-Service', serviceName);
setHeader(event, 'X-Cached', 'false');
return response;
});Conclusión
useRequestFetch es tu herramienta de servidor para:
- ✅ Proxy de APIs manteniendo contexto
- ✅ BFF Patterns agregando datos
- ✅ Autenticación preservando sesiones
- ✅ Rate limiting y control de peticiones
