Services Layer
This document explains how to create service classes that integrate with the @wisemen/vue-core-api-utils package. Services act as an abstraction layer between your API client and your queries/mutations, handling data transformation and error handling.
ApiUtil.fromPromise
The ApiUtil.fromPromise utility wraps API calls and converts them to ApiResult:
// src/utils/api/api.util.ts
import type { ApiResult } from '@wisemen/vue-core-api-utils'
import { ResultAsync } from 'neverthrow'
import { ApiErrorUtil } from './apiError.util'
export class ApiUtil {
static async fromPromise<T>(promise: PromiseLike<T>, message?: string): Promise<ApiResult<T>> {
return await ResultAsync.fromPromise(promise, (error) => {
return ApiErrorUtil.handleApiError({
error,
message,
})
})
}
}Service Method Patterns
Create Operation
Returns the UUID of the created resource:
static async create(form: ContactCreateForm): Promise<ApiResult<ContactUuid>> {
// Transform form data to API DTO
const dto = ContactCreateTransformer.toDto(form)
// Call API and wrap in ApiResult
const result = await ApiUtil.fromPromise(createContactV1({
body: dto,
}))
// Transform response and extract UUID
return result.map((res) => res.data.uuid as ContactUuid)
}Usage in Mutation:
export function useContactCreateMutation() {
return useMutation({
queryFn: async (queryOptions: { body: ContactCreateForm }) => {
return await ContactService.create(queryOptions.body)
},
queryKeysToInvalidate: {
contactIndex: {},
},
})
}Read Operation (Single Resource)
Returns the transformed detail model:
static async getByUuid(contactUuid: ContactUuid): Promise<ApiResult<ContactDetail>> {
// Call API with path parameters
const result = await ApiUtil.fromPromise(viewContactDetailV1({
path: {
uuid: contactUuid,
},
}))
// Transform DTO to domain model
return result.map((response) => {
return ContactDetailTransformer.fromDto(response.data)
})
}Usage in Query:
export function useContactDetailQuery(contactUuid: ComputedRef<ContactUuid>) {
return useQuery({
queryFn: () => ContactService.getByUuid(toValue(contactUuid)),
queryKey: {
contactDetail: {
contactUuid,
},
},
staleTime: TimeUtil.seconds(30),
})
}Read Operation (Paginated List)
Returns paginated data with metadata:
static async getAll(
params: OffsetPagination<ContactIndexQueryParams>,
): Promise<OffsetPaginationResult<ContactIndex>> {
// Transform query parameters to API format
const result = await ApiUtil.fromPromise(
viewContactIndexV1({
query: ContactIndexQueryParamsTransformer.toDto(params),
}),
)
// Transform response items and include pagination metadata
return result.map((response) => ({
data: response.data.items.map(ContactIndexTransformer.fromDto),
meta: response.data.meta,
}))
}Usage in Infinite Query:
export function useContactIndexQuery(options: InfiniteQueryOptions<ContactIndexQueryParams>) {
return useOffsetInfiniteQuery({
queryFn: (pagination) => ContactService.getAll({
filters: options.params.filters.value,
pagination,
search: options.params.search.value,
sort: options.params.sort.value,
}),
queryKey: {
contactIndex: {
queryParams: options.params,
},
},
})
}Update Operation
Returns void on success:
static async update(contactUuid: ContactUuid, form: ContactUpdateForm): Promise<ApiResult<void>> {
// Transform form to DTO
const dto = ContactUpdateTransformer.toDto(form)
// Call API with path parameters and body
const result = await ApiUtil.fromPromise(
updateContactV1({
body: dto,
responseValidator: undefined, // Skip response validation if no body expected
path: {
uuid: contactUuid,
},
}),
)
// Return void result
return result.map((res) => res.data)
}Usage in Mutation:
export function useContactUpdateMutation() {
return useMutation({
queryFn: async (queryOptions: {
body: ContactUpdateForm
params: { contactUuid: ContactUuid }
}) => {
return await ContactService.update(queryOptions.params.contactUuid, queryOptions.body)
},
queryKeysToInvalidate: {
contactDetail: {
contactUuid: (params) => params.contactUuid,
},
contactIndex: {},
},
})
}Delete Operation
Returns void on success:
static async delete(contactUuid: ContactUuid): Promise<ApiResult<void>> {
const result = await ApiUtil.fromPromise(
deleteContactV1({
responseValidator: undefined, // Skip response validation for delete
path: {
uuid: contactUuid,
},
}),
)
return result.map((res) => res.data)
}Usage in Mutation:
export function useContactDeleteMutation() {
return useMutation({
queryFn: async (queryOptions: { params: { contactUuid: ContactUuid } }) => {
return await ContactService.delete(queryOptions.params.contactUuid)
},
queryKeysToInvalidate: {
contactIndex: {},
},
})
}Complete Service Example
export class ContactService {
static async create(form: ContactCreateForm): Promise<ApiResult<ContactUuid>> {
const dto = ContactCreateTransformer.toDto(form)
const result = await ApiUtil.fromPromise(createContactV1({
body: dto,
}))
return result.map((res) => res.data.uuid as ContactUuid)
}
static async delete(contactUuid: ContactUuid): Promise<ApiResult<void>> {
const result = await ApiUtil.fromPromise(
deleteContactV1({
responseValidator: undefined,
path: {
uuid: contactUuid,
},
}),
)
return result.map((res) => res.data)
}
static async getAll(
params: OffsetPagination<ContactIndexQueryParams>,
): Promise<OffsetPaginationResult<ContactIndex>> {
const result = await ApiUtil.fromPromise(
viewContactIndexV1({
query: ContactIndexQueryParamsTransformer.toDto(params),
}),
)
return result.map((response) => ({
data: response.data.items.map(ContactIndexTransformer.fromDto),
meta: response.data.meta,
}))
}
static async getByUuid(contactUuid: ContactUuid): Promise<ApiResult<ContactDetail>> {
const result = await ApiUtil.fromPromise(viewContactDetailV1({
path: {
uuid: contactUuid,
},
}))
return result.map((response) => {
return ContactDetailTransformer.fromDto(response.data)
})
}
static async update(contactUuid: ContactUuid, form: ContactUpdateForm): Promise<ApiResult<void>> {
const dto = ContactUpdateTransformer.toDto(form)
const result = await ApiUtil.fromPromise(
updateContactV1({
body: dto,
responseValidator: undefined,
path: {
uuid: contactUuid,
},
}),
)
return result.map((res) => res.data)
}
}Data Transformation Flow
Outgoing Data (Request)
- Form/Model → Transformer.toDto() → DTO → API Client
const dto = ContactCreateTransformer.toDto(form)
const result = await ApiUtil.fromPromise(createContactV1({ body: dto }))Incoming Data (Response)
- API Client → DTO → Transformer.fromDto() → Domain Model
return result.map((response) => {
return ContactDetailTransformer.fromDto(response.data)
})Query Parameters
- Query Params Model → Transformer.toDto() → API Query Format
const result = await ApiUtil.fromPromise(
viewContactIndexV1({
query: ContactIndexQueryParamsTransformer.toDto(params),
}),
)Type Safety
Services ensure type safety throughout the entire data flow:
// Input is typed
static async create(form: ContactCreateForm): Promise<ApiResult<ContactUuid>> {
// ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^
// Form type Return type
const dto = ContactCreateTransformer.toDto(form)
// ^^^
// DTO type inferred from transformer
const result = await ApiUtil.fromPromise(createContactV1({ body: dto }))
// ^^^^^^
// ApiResult<APIResponse> inferred
return result.map((res) => res.data.uuid as ContactUuid)
// ^^^^^^^^^^^^
// Explicit type cast
}Error Handling
All service methods return ApiResult, forcing consumers to handle errors:
// In a mutation or query
const result = await ContactService.create(form)
// Must handle both success and error cases
result.match(
(contactUuid) => {
// Success case - contactUuid is typed as ContactUuid
console.log('Created contact:', contactUuid)
},
(error) => {
// Error case - error is typed as ApiError
console.error('Failed to create contact:', error)
}
)Integration with Queries and Mutations
Services are consumed by queries and mutations:
// Query uses service
export function useContactDetailQuery(contactUuid: ComputedRef<ContactUuid>) {
return useQuery({
queryFn: () => ContactService.getByUuid(toValue(contactUuid)),
queryKey: { contactDetail: { contactUuid } },
})
}
// Mutation uses service
export function useContactCreateMutation() {
return useMutation({
queryFn: async (queryOptions: { body: ContactCreateForm }) => {
return await ContactService.create(queryOptions.body)
},
queryKeysToInvalidate: { contactIndex: {} },
})
}Related Documentation
- Query Documentation - Single resource fetching with
useQuery - Paginated Query Documentation - Paginated data with infinite scrolling
- Mutations Documentation - Create, update, and delete operations
- Main Usage Documentation - Complete package overview