Client Entities
Client entities live in mymodule/client/entities/ and extend their shared counterpart with UI-specific logic — computed display properties, formatting helpers, or reactive state that only makes sense in the browser.
They never contain server-only dependencies and are safe to import in any Vue component.
When to Create a Client Entity
In most cases the shared entity is already enough. Only reach for a client entity when you have logic that truly belongs to the UI layer, for example:
- Formatting a value for display (labels, colors, badges)
- Deriving state from multiple fields for a component
- Adding UI-only flags like
isExpandedorisSelected
Creating a Client Entity
Place the file in mymodule/client/entities/ using the naming convention <name>.entity.ts. Extend the shared entity directly:
// mymodule/client/entities/item.entity.ts
import Base from '#mymodule/shared/entities/item.entity.ts'
import { composeWith } from '#shared/utils/compose.ts'
export default class Item extends composeWith(Base) {
public get displayName(): string {
return `[${this.id}] ${this.name}`
}
}Since the shared entity already includes BaseEntity, the client entity inherits from() and merge() without any extra setup.
Hydrating API Responses
Use the static from() method inherited from BaseEntity to turn a raw API response into a typed client entity instance:
import Item from '#mymodule/client/entities/item.entity.ts'
const response = await $fetch('/api/mymodule/items/1')
const item = Item.from(response)
console.log(item.displayName) // '[1] My Item'
console.log(item.created_at) // inherited from the shared entityUsing with Reactive State
Client entities work naturally with Vue's reactivity system. Wrap them in ref or reactive as needed:
import { ref } from 'vue'
import Item from '#mymodule/client/entities/item.entity.ts'
const item = ref<Item | null>(null)
async function loadItem(id: number) {
const response = await $fetch(`/api/mymodule/items/${id}`)
item.value = Item.from(response)
}For lists, hydrate each item using Array.map:
import Item from '#mymodule/client/entities/item.entity.ts'
const response = await $fetch('/api/mymodule/items')
const items = response.map(Item.from.bind(Item))Adding Display Helpers
Keep formatting logic in the entity instead of scattering it across components:
// mymodule/client/entities/item.entity.ts
import Base from '#mymodule/shared/entities/item.entity.ts'
import { composeWith } from '#shared/utils/compose.ts'
export default class Item extends composeWith(Base) {
public get statusColor(): string {
const map: Record<string, string> = {
pending: 'yellow',
active: 'green',
archived: 'gray',
}
return map[this.status] ?? 'gray'
}
public get formattedCreatedAt(): string {
return new Date(this.created_at).toLocaleDateString()
}
}Components stay clean and reference only the entity property:
<Badge :color="item.statusColor">{{ item.status }}</Badge>