Skip to content

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.


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 es el composable más usado del proyecto. Cumple tres funciones:

  1. Resuelve la función de servicio correcta desde la configuración de la vertical activa
  2. Ejecuta la llamada con el idioma actual del usuario
  3. Fallback i18n: Si los datos vienen vacíos o con campos sin traducir, reintenta con otro idioma y hace un merge profundo

Retorna { data, isPending } — se actualiza automáticamente con navegación:

const { data, isPending } = useServices('getCourses')
// data es Ref<Course[] | null>
// isPending es Ref<boolean>

El segundo argumento son los params que recibe la función del servicio:

const { data } = useServices('getCourseBySlug', { slug: route.params.slug })

Con { promise: true } retorna los datos directamente (para lógica imperativa):

const courses = await useServices('getCourses', undefined, { promise: true })
// courses es Course[] | null directamente

Para servicios de auth o donde no tiene sentido buscar en otro idioma:

const user = await useServices('getUserData', undefined, { useFallback: false })

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 null
4. 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.


Clase que envuelve $fetch de Nuxt con lógica específica del proyecto:

// GET
const data = await httpClient.get<Course[]>({
resource: 'courses', // → /api/courses
params: 'populate=*&locale=es' // query string Strapi
})
// POST
const 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 }).


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)

services/inspiria/courses.ts
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.


Cada función de servicio se registra explícitamente en el objeto de la vertical:

constants/verticals/inspiria.ts
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.


Las queries de Strapi son complejas (populate anidados, filters, sort). Se definen como objetos TypeScript tipados en constants/api-structures/:

constants/api-structures/coursesStructure.ts
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/.


interfaces/api/newFeature.d.ts
interface NewFeature {
id: number
title: string
description: string
slug: string
thumbnail: StrapiMedia
}
constants/api-structures/newFeature.ts
export const NEW_FEATURE_LIST_QUERY = 'populate[thumbnail]=*&sort=createdAt:desc'
export const NEW_FEATURE_DETAIL_QUERY = 'populate=*'
services/inspiria/newFeature.ts
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 }
constants/verticals/inspiria.ts
services: {
// ... servicios existentes
getNewFeatures: newFeatureService.getNewFeatures,
}
const { data, isPending } = useServices('getNewFeatures')

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.