useAPI
Overview
The useAPI composable is the primary mechanism for executing HTTP requests and managing API state in the ES-Profiler platform frontend (platform-frontend).
It replaces the legacy BaseApi utility class with a fully reactive, TypeScript-safe Vue composable.
useAPI integrates directly with Vue 3 reactivity (shallowRef, reactive), Axios, reactive Data and Query models (BaseDataModel, BaseListModel, BaseQuery), and authentication handlers.Key Features
- Automatic Type Safety: TypeScript autocomplete and type checking for registered endpoints via Vite type generation.
- Reactive State: Exposes
data,query,loading,loaded,success, anderrorstate. - Dynamic Path Interpolation: Automatically substitutes URL path parameters like
:productIdor:findingId. - Request Deduplication: Prevents duplicate
GETrequests with identical destination parameters. - Auto Abort: Automatically cancels in-flight HTTP requests when re-executed on the same composable instance.
- Built-in Authorization & Error Handling: Manages silent token authentication, automatic 401 signouts, and interactive 403 Permission Denied modal dialogs.
Defining API Endpoints (defineAPI)
API endpoint definitions are structured modularly in src/api/**/*.ts within the platform-frontend repository.
Each module exports a default APIConfig object defining the baseURL and endpoint registry entries.
Namespace Naming Convention
Endpoint namespaces are automatically derived from the file path relative to src/api/:
| File Path | Generated Namespace | Endpoint Key | Full Endpoint String |
|---|---|---|---|
src/api/feature.ts | feature | internal.create | 'feature.internal.create' |
src/api/meta.ts | meta | tags | 'meta.tags' |
src/api/insights/findings.ts | insights.findings | delete | 'insights.findings.delete' |
src/api/hub/interviews.ts | hub.interviews | list | 'hub.interviews.list' |
Example API Module
import { useConfig } from '@/composables/useConfig.ts'
import type { APIConfig } from '@/types/api'
const config: APIConfig = {
baseURL: useConfig('api.esp'),
endpoints: {
'internal.create': {
path: '/v1/inventory/internal/products/:productId/features',
method: 'post'
},
'internal.update': {
path: '/v1/inventory/internal/products/:productId/features/:featureId',
method: 'patch'
},
'internal.delete': {
path: '/v1/inventory/internal/products/features/:featureId',
method: 'delete'
}
}
}
export default config
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
path | string | (Required) | The endpoint URL template. Can include path parameters prefixed with a colon (e.g. :id). |
method | string | 'get' | HTTP method ('get', 'post', 'put', 'patch', 'delete'). |
model | Class | Object | null | undefined | Data model class (BaseDataModel subclass, BaseListModel, array, plain object, or null). |
query | Class | Object | undefined | Query model class (BaseQuery subclass or plain object) for managing search and filter parameters. |
multiple | boolean | Auto-detected | Set to true if response represents an array or collection. Automatically inferred if model is an Array or BaseListModel. |
authentication | boolean | true | When true, automatically waits for user authentication and injects the Authorization Bearer token header. |
Advanced APIConfig: model and query Patterns
When defining an API module, model and query options can be declared at the module root (APIConfig level) or overridden per endpoint.
1. Module-Level Defaults vs. Endpoint Overrides
- Module-Level Default: Declaring
modelorqueryat the root ofAPIConfigapplies that configuration as a fallback for all endpoints in the module. - Endpoint-Level Override: An endpoint can specify its own
modelorqueryto override the module-wide default.
2. Model Configuration Options
BaseDataModelSubclass (model: TaskHubInterviewModel):useAPIinstantiates and callssetup()on the class.- Array /
BaseListModel(model: [TaskHubInterviewModel]orProductListModel): Automatically flagsmultiple = trueand wraps items in aBaseListModel. - Plain Object Literal (
model: { url: '' }): For lightweight key-value payloads that do not require full model features. null(model: null): Explicitly specifies that an endpoint (e.g. DELETE or bodyless PATCH) does not send or expect a model body.
3. Query Configuration Options
BaseQuerySubclass (query: TaskHubInterviewQuery): Automatically instantiated and initialized byuseAPI.- Plain Object Literal (
query: { term: '', filter: 'active' }): Automatically wrapped byuseAPIinto aWrappedQueryinstance.
Real-World API Module Example
import { useConfig } from '@/composables/useConfig.ts'
import TaskHubInterviewQuery from '@/models/Api/Query/TaskHubInterviewQuery.ts'
import { TaskHubInterviewModel } from '@/models/Data/TaskHub/TaskHubInterviewModel.ts'
import { TaskHubInterviewStatsModel } from '@/models/Data/TaskHub/TaskHubInterviewStatsModel.ts'
import type { APIConfig } from '@/types/api'
const config: APIConfig = {
baseURL: useConfig('api.esp'),
// Module-wide default model: inherited by 'list' and 'show' below
model: TaskHubInterviewModel,
endpoints: {
// 1. Uses module-level TaskHubInterviewModel + query class + multiple collection flag
list: {
path: '/v1/user-tasks',
multiple: true,
query: TaskHubInterviewQuery
},
// 2. Automatically inherits module-level TaskHubInterviewModel
show: {
path: '/v1/user-tasks/:interviewId'
},
// 3. Overrides model with a lightweight plain object literal
link: {
path: '/v1/user-tasks/:interviewId/url',
model: { url: '' }
},
// 4. Overrides model with a specific BaseDataModel class + custom query model
stats: {
path: '/v1/user-tasks/stats',
model: TaskHubInterviewStatsModel,
query: TaskHubInterviewQuery
},
// 5. Overrides model with null for actions without a body payload
'group.delete': {
path: '/v1/user-tasks/groups/:groupId',
method: 'delete',
model: null
},
// 6. Overrides model with null for action patch
'group.reminder': {
path: '/v1/user-tasks/groups/:groupId/emails',
method: 'patch',
model: null
}
}
}
export default config
Automatic Type Safety (vite-plugin-api-types)
The platform uses a custom Vite plugin (src/plugins/api/vite.mts) to auto-generate TypeScript definitions for registered endpoints.
vite dev) and build (vite build), the plugin watches src/api/**/*.ts and updates api-registry.d.ts in the project root. This populates the global APIRegistry interface, giving IDE autocomplete and type errors for invalid API keys.// Autocomplete suggests valid endpoints such as 'meta.tags', 'feature.internal.create', etc.
const tags = useAPI('meta.tags')
Data Models (BaseDataModel & BaseListModel)
Data Models define the structure, default values, and mutation behaviors of entities returned by or sent to backend APIs. useAPI automatically instantiates and binds response resource objects to these data models.
Subclassing BaseDataModel
To create a single-entity data model, extend BaseDataModel:
import BaseDataModel from '@/models/Data/Utility/BaseDataModel'
export default class ProductModel extends BaseDataModel {
id: string = ''
name: string = ''
description: string = ''
active: boolean = true
constructor() {
super()
}
}
Automatic Nested BaseListModel Instantiation
When defining nested model relationships in a BaseDataModel, you can declare an array attribute initialized with the target model class or instance (e.g. products = [ProductModel]).
During setup(), BaseDataModel automatically converts array properties containing a BaseDataModel class constructor or instance into an auto-instantiated BaseListModel:
import BaseDataModel from '@/models/Data/Utility/BaseDataModel'
import ProductModel from '@/models/Data/Inventory/ProductModel'
export default class VendorModel extends BaseDataModel {
id: string = ''
name: string = ''
// Declaring [ProductModel] automatically converts 'products' into a BaseListModel<ProductModel>
products = [ProductModel]
constructor() {
super()
}
}
setup() or assign() runs, VendorModel.products is automatically transformed into new BaseListModel(ProductModel). Any incoming JSON data for products will then be instantiated and cast as ProductModel items within the BaseListModel.Model Lifecycle & State Tracking
BaseDataModel tracks internal snapshots of model state to support dirty-checking, rollback, and selective property serialization:
__original: Snapshot of the object state aftersetup()is first called.__previous: Snapshot of the last committed or assigned state.isDirty: Getter returningtrueif current field values differ from__previous.postable: Getter returning only enumerable properties suitable for sending inPOST/PUT/PATCHrequest bodies.
Data Model Methods
| Method | Signature | Description |
|---|---|---|
setup(custom?, warn?) | (custom?: Partial<this>, warn?: boolean) => this | Initializes nested list models, applies custom overrides, and records initial __original / __previous snapshots. |
assign(data, cast?, update?) | (data?: Partial<this>, cast?: boolean, update?: boolean) => this | Merges new property values into the model instance, optionally casting types and recording __previous. |
update(data, cast?) | (data?: Partial<this>, cast?: boolean) => this | Alias for assign with update = true enabled. |
commited() | () => this | Commits the current property state as the new __previous reference, clearing the isDirty flag after a successful save. |
undo() | () => this | Reverts modified property values back to the __previous snapshot. |
reset(data?, hard?) | (data?: Partial<this>, hard?: boolean) => this | Reverts the model to __original snapshot values, optionally overriding fields. Performs a commited() if hard = true. |
const product = new ProductModel().setup({ name: 'Draft Product' })
product.name = 'New Name'
console.log(product.isDirty) // true
product.undo()
console.log(product.name) // 'Draft Product'
Handling Collections (BaseListModel)
When an endpoint returns a list of items or has multiple: true, useAPI wraps the model in a BaseListModel.
import BaseListModel from '@/models/Data/Utility/BaseListModel'
import ProductModel from '@/models/Data/Inventory/ProductModel'
export default class ProductListModel extends BaseListModel<typeof ProductModel> {
constructor() {
super(ProductModel)
}
}
Key BaseListModel Properties & Methods
list: Array of strongly typedBaseDataModelinstances.total: Number of total items available on the backend (populated from paginated API responses).pages: Total page count returned by paginated API responses.assign(items): Instantiates and appends or replaces items inlist.withCache(identifier): Enables result caching using a specific key (e.g.'id').clear(): Empties thelistarray and resetstotalto0.reset(items): Re-assigns items and resets pagination metadata.
Query Models (BaseQuery & PaginationQuery)
Query Models manage request search filters, sorting options, pagination parameters, and synchronization with Vue Router URL query strings.
Subclassing BaseQuery
Extend BaseQuery to construct search filter models for non-paginated endpoints:
import BaseQuery from '@/models/Api/Utility/BaseQuery'
export default class FilterQuery extends BaseQuery {
category: string = ''
status: string = 'active'
constructor() {
super('filter') // 'filter' is the query scope prefix
}
}
Query Scoping & Router Synchronization
Query models support scoping (__scope) to avoid query parameter collisions when multiple components share URL query parameters:
- Scoped URL Output: When a scope is supplied (e.g.
tags), parameters are rendered with a delimiter in the URL:?tags~searchTerm=security. routable: Getter returning sanitized query parameter pairs ready for Vue Router.go(router, absolute?): Updates Vue Router URL query string with current query model properties.parse(payload, props?, scope?): Deserializes route query strings or parameter objects directly into model properties.
Deferred State (Draft Filters)
Query models provide a deferred state (deferred) so users can adjust filter controls in a modal or drawer without immediately triggering network requests:
const query = new TagsQuery()
// User edits draft state in form
query.deferred.searchTerm = 'compliance'
// Apply draft changes to active query state
query.commit()
// Or cancel draft changes
query.cancel()
Query State Control Methods
| Method | Description |
|---|---|
commit() | Copies values from deferred draft state into active query fields and updates filter count. |
cancel() | Reverts deferred draft state back to active query parameters. |
undo() | Reverts deferred draft state to the __previous query state snapshot. |
reset() | Reverts deferred and active query state to initial __original default values. |
Paginated Queries (PaginationQuery)
For endpoints supporting page numbers, page sizes, and sorting, extend PaginationQuery:
import PaginationQuery from '@/models/Api/Query/PaginationQuery'
export default class TagsQuery extends PaginationQuery {
type: string = ''
searchTerm: string = ''
constructor() {
super('tags')
}
}
Built-in PaginationQuery Parameters
page: 1-indexed page number (automatically converted to 0-indexed for backend API requests inconfiguration).size: Page size limit (defaults to20).sortBy: Array of sort objects:[{ key: 'name', order: 'asc' }]. Automatically serialized tosort: ["name~A"].groupBy: Array of grouping items.search: General search string.configuration: Cleansed object passed directly to Axios request parameters byuseAPI.
Using useAPI in Vue Components
1. Destructuring State (Standard Pattern)
Best for components that destructure reactive state and trigger execution explicitly or in lifecycle hooks:
<script setup lang="ts">
import { onMounted } from 'vue'
import { useAPI } from '@/composables/useAPI'
const { data, query, execute, loading } = useAPI('meta.tags')
onMounted(() => {
execute()
})
</script>
<template>
<v-select
v-model="query.tag"
:items="data"
:loading="loading"
label="Filter by Tag"
/>
</template>
2. Reactive Wrapper Pattern
Best when passing the entire API instance into templates or computed properties:
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useAPI } from '@/composables/useAPI'
const signals = reactive(useAPI('insights.signals.list'))
onMounted(() => {
signals.execute()
})
</script>
<template>
<v-progress-linear v-if="signals.loading" indeterminate />
<div v-else-if="signals.loaded">
<div v-for="item in signals.data" :key="item.id">
{{ item.title }}
</div>
</div>
</template>
3. Per-Instance Customization (Call-Site options)
When calling useAPI('namespace', options), you can pass per-instance options to override or customize the endpoint's default model or query configuration at the call site:
const api = useAPI('example', {
model: { key: 'value' },
query: { search: 'term' }
})
This is particularly useful when a component requires a specific initial payload shape, custom query filters, or isolated query scoping.
A. Overriding or Providing Initial model Payload
You can pass a static BaseDataModel class constructor, a custom object literal, or a pre-configured model instance:
// 1. Passing a static BaseDataModel class constructor (auto-instantiated and setup by useAPI)
const productApi = useAPI('product.internal.update', {
model: ProductModel
})
// 2. Passing a custom object literal as the initial model payload
const featureCreate = useAPI('feature.internal.create', {
model: { name: 'New Feature', description: '' }
})
// 3. Passing a pre-configured BaseDataModel instance
const customProduct = new ProductModel().setup({ active: true })
const customProductApi = useAPI('product.internal.update', {
model: customProduct
})
B. Overriding or Providing Custom query Parameters
You can supply per-instance query options as a static BaseQuery class constructor, a plain object literal, or a pre-configured BaseQuery instance:
// 1. Passing a static BaseQuery class constructor (auto-instantiated by useAPI)
const tagsApi = useAPI('meta.tags', {
query: TagsQuery
})
// 2. Passing a plain query object literal (automatically wrapped in WrappedQuery)
const customTagsApi = useAPI('meta.tags', {
query: { type: 'system', searchTerm: 'security' }
})
// 3. Passing a pre-configured, scoped BaseQuery instance
const dialogQuery = new TagsQuery('dialogScope')
dialogQuery.searchTerm = 'audit'
const scopedTagsApi = useAPI('meta.tags', {
query: dialogQuery
})
C. Passing Static Class Constructors for Both model and query
You can simply pass both static class constructors to useAPI. useAPI will automatically instantiate and initialize both the model and query instances:
const productApi = useAPI('product.internal.update', {
model: ProductModel,
query: ProductQuery
})
D. Combining Object Literals or Custom Overrides
const directiveApi = useAPI('settings.directive.list', {
model: { key: 'theme', value: 'dark' },
query: { category: 'user-preference', limit: 10 }
})
useAPI(namespace, options) take precedence over the endpoint defaults registered in src/api/*.ts.API Reference
Invoking useAPI(namespace, options?) returns an ApiInstance object containing the following properties and methods:
| Member | Type | Description |
|---|---|---|
data | BaseDataModel | BaseListModel | Object | Array | Reactive data instance populated with the response resource upon request completion. |
query | BaseQuery | Reactive query instance holding URL search parameters. |
loading | ShallowRef<boolean> | true while an HTTP request is in-flight. |
loaded | ShallowRef<boolean> | true after at least one successful request has completed. |
success | ShallowRef<boolean> | true if the last executed request succeeded. |
error | ResponseErrorHandler | Error handler instance containing active response errors. |
execute(options?, force?) | (options?: APIExecOptions, force?: boolean) => Promise<any> | Executes the HTTP request. Accepts parameter overrides, custom Axios config, or model payloads. |
refresh() | () => Promise<any> | Re-executes the previous request with saved attributes, forcing a fresh network call (bypassing deduplication). |
reset(data?, hard?) | (data?: any, hard?: boolean) => void | Resets loaded to false, resets data model, and clears error state. |
undo() | () => void | Reverts modified values on data model if supported by the model instance. |
recallable() | () => void | Clears the request deduplication memory for this endpoint. |
Core Behaviors & Architecture
Path Parameter Interpolation
If an endpoint path includes parameters prefixed with a colon (e.g. :productId), useAPI resolves these values at execution time.
Parameters are resolved from:
- The
paramsobject passed toexecute({ params: { productId: 123 } }) - Properties on the
datainstance matching the parameter key name
const patcher = useAPI('insights.signals.patch')
// Path: /v1/insights/signals/:signalId
await patcher.execute({
params: { signalId: 'sig_123' },
model: { title: 'Updated Title' }
})
Request Deduplication
To minimize unnecessary network traffic, useAPI automatically deduplicates identical GET requests sent to the exact same URL and query string.
GET request is triggered with a destination URL identical to the immediate previous request, useAPI skips execution and logs a console warning in debug mode.To bypass deduplication and force a network request, pass force = true to execute() or use refresh():
// Bypasses deduplication
await api.refresh()
// Or explicitly force execution
await api.execute({}, true)
Automatic Request Cancellation
If execute() is called on a useAPI instance while a previous request for the same endpoint is still pending, useAPI automatically aborts the previous in-flight request via AbortController.
Authentication & Error Handling
- Token Injection:
useAPIcallswaitUntilAuthed()and attaches theAuthorization: Bearer <token>header automatically before sending authenticated requests. - 401 Unauthorized: Automatically triggers silent sign-out (
signoutSilent()) to re-authenticate the user session. - 403 Forbidden: Intercepts HTTP 403 responses and presents an interactive
Confirmation.Acknowledge()modal detailing:- Target HTTP method and endpoint URL
- User identity (Email, Client Type, Client ID, Organisation ID)
- Assigned security policy actions
Code Examples
Form Submission with Payload
<script setup lang="ts">
import { ref } from 'vue'
import { useAPI } from '@/composables/useAPI'
const props = defineProps<{ productId: string }>()
const emit = defineEmits(['saved'])
const featureCreate = useAPI('feature.internal.create', {
model: { name: '', description: '' }
})
const save = async () => {
try {
await featureCreate.execute({
params: { productId: props.productId }
})
emit('saved')
} catch (err) {
// Error is automatically populated in featureCreate.error
}
}
</script>
<template>
<v-card>
<v-card-text>
<v-text-field v-model="featureCreate.data.name" label="Feature Name" />
<v-textarea v-model="featureCreate.data.description" label="Description" />
</v-card-text>
<v-card-actions>
<v-btn
:loading="featureCreate.loading"
color="primary"
@click="save"
>
Create Feature
</v-btn>
</v-card-actions>
</v-card>
</template>
Deleting Resources with Parameters
const deletion = useAPI('insights.findings.delete')
const removeFinding = async (findingId: string) => {
await deletion.execute({
params: { findingId }
})
}

