Servicios y useServices
La capa de servicios es la responsable de todas las llamadas HTTP del proyecto. Está diseñada con inversión de dependencias para que los componentes no conozcan los detalles de la API.
Flujo completo
Section titled “Flujo completo”Componente → useServices('getCourses') → busca en visibleVertical.services.getCourses → coursesService.getCourses(lang) → httpClient.get({ resource: 'courses', params: QUERY }) → $fetch('https://api.inspiriadental.com/api/courses?...')useServices — El composable central
Section titled “useServices — El composable central”useServices es el composable más usado del proyecto. Cumple tres funciones:
- Resuelve la función de servicio correcta desde la configuración de la vertical activa
- Ejecuta la llamada con el idioma actual del usuario
- Fallback i18n: Si los datos vienen vacíos o con campos sin traducir, reintenta con otro idioma y hace un merge profundo
Modo reactivo (default)
Section titled “Modo reactivo (default)”Retorna { data, isPending } — se actualiza automáticamente con navegación:
const { data, isPending } = useServices('getCourses')// data es Ref<Course[] | null>// isPending es Ref<boolean>Con parámetros
Section titled “Con parámetros”El segundo argumento son los params que recibe la función del servicio:
const { data } = useServices('getCourseBySlug', { slug: route.params.slug })Modo promesa (directo)
Section titled “Modo promesa (directo)”Con { promise: true } retorna los datos directamente (para lógica imperativa):
const courses = await useServices('getCourses', undefined, { promise: true })// courses es Course[] | null directamenteSin fallback de idioma
Section titled “Sin fallback de idioma”Para servicios de auth o donde no tiene sentido buscar en otro idioma:
const user = await useServices('getUserData', undefined, { useFallback: false })Fallback de idioma (merge profundo)
Section titled “Fallback de idioma (merge profundo)”Esta es la característica más importante. Permite que el CMS tenga traducciones parciales sin romper la UI:
1. Llama servicio con locale actual (ej: 'en')2. Recibe datos — algunos campos pueden estar vacíos ("")3. Detecta campos vacíos o null4. Llama nuevamente con idioma fallback (ej: 'es')5. Hace merge recursivo: - Mantiene valores del idioma primario donde hay datos - Rellena campos vacíos con el fallback - Funciona en profundidad (objetos anidados, arrays)Ejemplo práctico: un curso con title traducido al inglés pero description vacía → la description se mostrará en español automáticamente.
HttpClient (fetchApi.ts)
Section titled “HttpClient (fetchApi.ts)”Clase que envuelve $fetch de Nuxt con lógica específica del proyecto:
// GETconst data = await httpClient.get<Course[]>({ resource: 'courses', // → /api/courses params: 'populate=*&locale=es' // query string Strapi})
// POSTconst data = await httpClient.post<LoginResponse>({ resource: 'user-endpoints/login', body: { identifier: email, password }})URL base: Se toma automáticamente de visibleVertical.productionApi o visibleVertical.developmentApi según el entorno. No la configuras tú.
Formato final: ${baseUrl}/api/${resource}?${params}
Manejo de errores: Si la respuesta es un array vacío o tiene propiedad error, lanza un createError({ statusCode: 404 }).
Estructura de carpetas de servicios
Section titled “Estructura de carpetas de servicios”services/├── fetchApi.ts → HttpClient + ZohoClient (clases HTTP singleton)├── strapi/ → Autenticación (login, registro, passwords)├── inspiria/ → Contenido específico (channels, courses, books, lives...)└── shared/ → Compartidos entre verticales (checkout, orders, products)Anatomía de un servicio
Section titled “Anatomía de un servicio”import type { LanguageCode } from '~/types/common'import { httpClient } from '../fetchApi'import { COURSES_STRUCTURE } from '~/constants/api-structures/coursesStructure'import qs from 'qs'
const coursesQuery = qs.stringify( { populate: COURSES_STRUCTURE.populate, fields: COURSES_STRUCTURE.fields }, { encodeValuesOnly: true },)
async function getCourses(lang: LanguageCode): Promise<Course[] | null> { return httpClient.get<Course[]>({ resource: 'courses', params: `${coursesQuery}&locale=${lang}`, })}
async function getCourseBySlug( lang: LanguageCode, params: { slug: string },): Promise<Course | null> { const query = qs.stringify( { populate: COURSES_STRUCTURE.populate, filters: { slug: { $eq: params.slug } }, }, { encodeValuesOnly: true }, )
return httpClient.get<Course>({ resource: 'courses', params: `${query}&locale=${lang}`, })}
export const coursesService = { getCourses, getCourseBySlug }Patrón clave: Todo servicio recibe lang: LanguageCode como primer argumento (useServices lo inyecta automáticamente) y opcionalmente params como segundo.
Registro en la vertical
Section titled “Registro en la vertical”Cada función de servicio se registra explícitamente en el objeto de la vertical:
import { coursesService } from '~/services/inspiria/courses'
export const INSPIRIA: Verticals = { name: 'Inspiria', services: { getCourses: coursesService.getCourses, getCourseBySlug: coursesService.getCourseBySlug, getChannels: channelsService.getChannels, // ... 30+ servicios registrados }}Esto es el “contrato” entre componentes y APIs: los componentes solo conocen el nombre ('getCourses'), la implementación concreta la decide la vertical.
API Structures (Queries de Strapi)
Section titled “API Structures (Queries de Strapi)”Las queries de Strapi son complejas (populate anidados, filters, sort). Se definen como objetos TypeScript tipados en constants/api-structures/:
import type { Strapi5RequestPopulateParam } from '@nuxtjs/strapi'import { SEO, EXPERTS_PARTNERS_RELATION_STRUCTURE } from './commonStructure'import type { CourseCard } from '~/interfaces/api/strapi/courses'
export const COURSES_STRUCTURE = { fields: ['id', 'title', 'slug', 'type', 'SKU', 'purchaseType'], populate: { image: { fields: ['url', 'alternativeText'] }, experts: EXPERTS_PARTNERS_RELATION_STRUCTURE, fullPrice: { fields: ['price', 'discountPercentage', 'discountPrice', 'stripePriceID'] }, categories: { fields: ['id', 'name', 'slug'] }, seo: SEO, },} as Strapi5RequestPopulateParam<CourseCard>Luego se serializan con qs.stringify en el servicio (ver anatomía de un servicio arriba).
Regla: Nunca escribas queries Strapi como strings inline. Siempre usa objetos tipados en constants/api-structures/.
Crear un nuevo servicio paso a paso
Section titled “Crear un nuevo servicio paso a paso”1. Define el tipo de respuesta
Section titled “1. Define el tipo de respuesta”interface NewFeature { id: number title: string description: string slug: string thumbnail: StrapiMedia}2. Crea la query de Strapi
Section titled “2. Crea la query de Strapi”export const NEW_FEATURE_LIST_QUERY = 'populate[thumbnail]=*&sort=createdAt:desc'export const NEW_FEATURE_DETAIL_QUERY = 'populate=*'3. Crea el servicio
Section titled “3. Crea el servicio”import { httpClient } from '../fetchApi'import { NEW_FEATURE_LIST_QUERY } from '~/constants/api-structures/newFeature'
async function getNewFeatures(lang: LanguageCode): Promise<NewFeature[] | null> { return httpClient.get<NewFeature[]>({ resource: 'new-features', params: `${NEW_FEATURE_LIST_QUERY}&locale=${lang}` })}
export const newFeatureService = { getNewFeatures }4. Registra en la vertical
Section titled “4. Registra en la vertical”services: { // ... servicios existentes getNewFeatures: newFeatureService.getNewFeatures,}5. Usa en componentes
Section titled “5. Usa en componentes”const { data, isPending } = useServices('getNewFeatures')ZohoClient (envío al CRM)
Section titled “ZohoClient (envío al CRM)”Para enviar datos a Zoho CRM (leads, eventos de usuario):
import { zohoClient } from '~/services/fetchApi'
await zohoClient.post({ resource: visibleVertical.zohoConfig.registerWebhook, body: { name, email, phone }})Es fire-and-forget (no retorna datos, solo notifica al CRM). Se usa en registro, contacto, y eventos de compra.