← Back to list

feature-development
by DevXoje
⭐ 0🍴 0📅 Jan 18, 2026
SKILL.md
name: feature-development description: > Patterns and conventions for implementing complete features following Clean Architecture in GeroCare. Trigger: When creating new features, domain entities, repositories, composables, or business logic components. license: Apache-2.0 metadata: author: gero-cloud version: "1.0" scope: [root] auto_invoke: "Creating new features or business logic"
When to Use
Use this skill when creating:
- New business features (medication, care-plans, incidents, shifts, etc.)
- Domain entities and business logic
- Repository interfaces and implementations
- Application composables (use cases)
- Feature-specific Vue components
- Pinia stores for feature state
Don't use this skill for:
- Agnostic UI components (use
ui-componentsskill) - Testing (use
testingskill) - Infrastructure configuration (Firebase, Docker)
Clean Architecture Structure
All features follow this structure:
src/business/{feature-name}/
├── domain/ # Core business logic
│ ├── {Entity}.ts # Domain entities
│ ├── {Entity}Repository.ts # Repository interface
│ └── {Entity}Errors.ts # Error types and factories
├── app/ # Application layer (use cases)
│ ├── use{Entity}.ts # Main composable
│ └── use{Entity}Form.ts # Form-specific composable
├── infrastructure/ # External services
│ ├── Firestore{Entity}Repository.ts # Firestore implementation
│ ├── index.ts # Repository factory
│ └── __tests__/ # Infrastructure tests
├── presentation/ # Vue components
│ ├── components/ # Feature-specific components
│ ├── pages/ # Feature pages
│ └── __tests__/ # Component tests
├── store.ts # Pinia store (optional)
└── routes.ts # Feature routes
Critical Patterns
1. Domain Layer
Entity Pattern:
// domain/{Entity}.ts
export interface Entity {
id: string
// Domain properties
createdAt: Date
updatedAt: Date
}
// Option 1: Manual validation (legacy)
export function validateEntity(entity: Entity): Result<Entity, string> {
// Validation logic
if (!entity.name) {
return Err('name is required')
}
return Ok(entity)
}
// Option 2: Zod validation (recommended) - See `zod` skill
// Use Zod schemas defined in domain/{Entity}.schema.ts or domain/{Entity}.ts
Repository Interface:
// domain/{Entity}Repository.ts
import type { Result } from '@/shared/domain/Result'
import type { EntityError } from './{Entity}Errors'
import type { Entity } from './{Entity}'
export interface EntityRepository {
create(entity: Omit<Entity, 'id' | 'createdAt' | 'updatedAt'>): Promise<Result<Entity, EntityError>>
findById(id: string): Promise<Result<Entity | null, EntityError>>
findAll(): Promise<Result<Entity[], EntityError>>
update(id: string, updates: Partial<Omit<Entity, 'id' | 'createdAt'>>): Promise<Result<Entity, EntityError>>
delete(id: string): Promise<Result<void, EntityError>>
}
Error Types:
// domain/{Entity}Errors.ts
export interface EntityError {
code: string
message: string
}
export function createEntityNotFoundError(message: string = 'Entity not found'): EntityError {
return { code: 'ENTITY_NOT_FOUND', message }
}
export function createEntityValidationError(message: string): EntityError {
return { code: 'ENTITY_VALIDATION_ERROR', message }
}
export function createUnknownEntityError(message: string = 'Unknown error'): EntityError {
return { code: 'UNKNOWN_ENTITY_ERROR', message }
}
2. Infrastructure Layer
Firestore Repository Pattern:
// infrastructure/Firestore{Entity}Repository.ts
import { collection, doc, addDoc, getDoc, updateDoc, deleteDoc, query, where, Timestamp } from 'firebase/firestore'
import type { Firestore } from 'firebase/firestore'
import type { EntityRepository } from '../domain/{Entity}Repository'
import type { Entity } from '../domain/{Entity}'
import type { EntityError } from '../domain/{Entity}Errors'
import { type Result, Ok, Err } from '@/shared/domain/Result'
import { createEntityNotFoundError, createUnknownEntityError } from '../domain/{Entity}Errors'
// Convert Firestore Timestamp to Date
function timestampToDate(timestamp: TimestampLike): Date {
if (timestamp instanceof Timestamp) return timestamp.toDate()
if (timestamp instanceof Date) return timestamp
if (timestamp && typeof timestamp === 'object' && 'toDate' in timestamp) {
return timestamp.toDate()
}
return new Date(timestamp as string)
}
// Convert Date to Firestore Timestamp
function dateToTimestamp(date: Date): Timestamp {
return Timestamp.fromDate(date)
}
// Convert Firestore document to Entity
function firestoreDocToEntity(docId: string, data: Record<string, unknown>): Entity {
return {
id: docId,
// Map Firestore data to Entity
createdAt: timestampToDate(data.createdAt as TimestampLike),
updatedAt: timestampToDate(data.updatedAt as TimestampLike),
}
}
export function createEntityRepository(db: Firestore): EntityRepository {
const collectionName = '{entities}'
async function create(entity: Omit<Entity, 'id' | 'createdAt' | 'updatedAt'>): Promise<Result<Entity, EntityError>> {
try {
const now = new Date()
const entityData = {
...entity,
createdAt: dateToTimestamp(now),
updatedAt: dateToTimestamp(now),
}
const docRef = await addDoc(collection(db, collectionName), entityData)
const createdEntity: Entity = {
...entity,
id: docRef.id,
createdAt: now,
updatedAt: now,
}
return Ok(createdEntity)
} catch (error) {
return Err(createUnknownEntityError('Failed to create entity'))
}
}
async function findById(id: string): Promise<Result<Entity | null, EntityError>> {
try {
const docRef = doc(db, collectionName, id)
const docSnap = await getDoc(docRef)
if (!docSnap.exists()) {
return Ok(null)
}
const entity = firestoreDocToEntity(docSnap.id, docSnap.data())
return Ok(entity)
} catch (error) {
return Err(createUnknownEntityError('Failed to find entity'))
}
}
// ... other methods
}
Repository Factory:
// infrastructure/index.ts
import { db } from '@/infrastructure/firebase/firebase.config'
import { createEntityRepository as createFirestoreEntityRepository } from './FirestoreEntityRepository'
import type { EntityRepository } from '../domain/{Entity}Repository'
export function createEntityRepository(): EntityRepository {
return createFirestoreEntityRepository(db)
}
3. App Layer (Composables)
Main Composable Pattern:
// app/use{Entity}.ts
import { ref, computed } from 'vue'
import { createEntityRepository } from '../infrastructure'
import type { Entity } from '../domain/{Entity}'
import type { EntityError } from '../domain/{Entity}Errors'
import { useAuthStore } from '@/business/auth/store'
const repository = createEntityRepository()
export function useEntity() {
const authStore = useAuthStore()
const entities = ref<Entity[]>([])
const entity = ref<Entity | null>(null)
const isLoading = ref(false)
const error = ref<EntityError | null>(null)
const loadEntities = async () => {
if (!authStore.user) {
error.value = { code: 'AUTH_ERROR', message: 'User not authenticated' }
return
}
isLoading.value = true
error.value = null
try {
const result = await repository.findAll()
if (result.success) {
entities.value = result.value
} else {
error.value = result.error
}
} catch (err) {
error.value = { code: 'UNKNOWN_ERROR', message: 'Failed to load entities' }
} finally {
isLoading.value = false
}
}
const loadEntity = async (id: string) => {
isLoading.value = true
error.value = null
try {
const result = await repository.findById(id)
if (result.success) {
entity.value = result.value
} else {
error.value = result.error
}
} catch (err) {
error.value = { code: 'UNKNOWN_ERROR', message: 'Failed to load entity' }
} finally {
isLoading.value = false
}
}
return {
entities: computed(() => entities.value),
entity: computed(() => entity.value),
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
loadEntities,
loadEntity,
}
}
Form Composable Pattern:
// app/use{Entity}Form.ts
import { ref, computed } from 'vue'
import { createEntityRepository } from '../infrastructure'
import type { Entity } from '../domain/{Entity}'
import { useAuthStore } from '@/business/auth/store'
const repository = createEntityRepository()
export function useEntityForm() {
const authStore = useAuthStore()
const form = ref<Partial<Entity>>({})
const isLoading = ref(false)
const error = ref<string | null>(null)
const isFormValid = computed(() => {
// Validation logic
return !!form.value.name
})
const submit = async () => {
if (!isFormValid.value || !authStore.user) return
isLoading.value = true
error.value = null
try {
const entityData = {
...form.value,
// Add required fields
} as Omit<Entity, 'id' | 'createdAt' | 'updatedAt'>
const result = await repository.create(entityData)
if (result.success) {
form.value = {}
return result.value
} else {
error.value = result.error.message
return null
}
} catch (err) {
error.value = 'Failed to create entity'
return null
} finally {
isLoading.value = false
}
}
return {
form,
isLoading: computed(() => isLoading.value),
error: computed(() => error.value),
isFormValid,
submit,
}
}
4. Result Type Pattern
Always use Result types for error handling:
import { type Result, Ok, Err } from '@/shared/domain/Result'
// In repository methods
async function create(entity: Entity): Promise<Result<Entity, EntityError>> {
try {
// ... success logic
return Ok(createdEntity)
} catch (error) {
return Err(createUnknownEntityError('Failed to create entity'))
}
}
// In composables
const result = await repository.create(entityData)
if (result.success) {
// Handle success: result.value
} else {
// Handle error: result.error
}
5. Firestore Date Conversion
Always convert dates when mapping between Firestore and domain:
type TimestampLike = Timestamp | Date | string | { toDate?: () => Date }
function timestampToDate(timestamp: TimestampLike): Date {
if (timestamp instanceof Timestamp) return timestamp.toDate()
if (timestamp instanceof Date) return timestamp
if (timestamp && typeof timestamp === 'object' && 'toDate' in timestamp) {
return timestamp.toDate()
}
return new Date(timestamp as string)
}
function dateToTimestamp(date: Date): Timestamp {
return Timestamp.fromDate(date)
}
6. Store vs Composable Decision
| Use Case | Use |
|---|---|
| Simple CRUD operations | Composable (app/use{Entity}.ts) |
| Complex state management | Store (store.ts) |
| Cross-feature state | Store |
| Feature-specific state | Composable (preferred) |
Prefer composables for feature-specific state. Use stores only when:
- Multiple features share state
- Complex state management is required
- You need Pinia's devtools/debugging features
Complete Feature Example
Directory Structure
src/business/medication/
├── domain/
│ ├── Medication.ts
│ ├── MedicationRepository.ts
│ └── MedicationErrors.ts
├── app/
│ ├── useMedication.ts
│ └── useMedicationForm.ts
├── infrastructure/
│ ├── FirestoreMedicationRepository.ts
│ └── index.ts
├── presentation/
│ ├── components/
│ │ ├── MedicationList.vue
│ │ └── MedicationForm.vue
│ └── pages/
│ └── MedicationPage.vue
├── store.ts (optional)
└── routes.ts
Step-by-Step Implementation
- Domain Layer: Create entity, repository interface, error types
- Infrastructure Layer: Implement Firestore repository with date conversion
- App Layer: Create composables using repository
- Presentation Layer: Build Vue components using composables
- Routes: Define feature routes in
routes.ts
Firestore Collection Naming
- Use plural, lowercase names:
residents,medications,incidents - Match feature name:
care-plans→carePlanscollection - Always index by
createdAtorupdatedAtfor queries
Error Handling Best Practices
- Always use Result types in repository methods
- Create specific error factories in
{Entity}Errors.ts - Handle errors in composables and expose them to components
- Show user-friendly messages in presentation layer
// Domain: Specific error types
export function createEntityNotFoundError(message?: string): EntityError
// Infrastructure: Return Result with error
const result = await repository.findById(id)
if (!result.success) {
return Err(createEntityNotFoundError('Entity not found'))
}
// App: Handle error in composable
if (!result.success) {
error.value = result.error
return
}
// Presentation: Show error to user
<p v-if="error" class="error">{{ error.message }}</p>
Resources
- Result Type:
src/shared/domain/Result.ts - Firebase Config:
src/infrastructure/firebase/firebase.config.ts - Zod Validation: See
zodskill for validation schema patterns - Example Feature:
src/business/residents/ - Example Domain:
src/business/residents/domain/ - Example Infrastructure:
src/business/residents/infrastructure/ - Example App:
src/business/residents/app/ - Example Presentation:
src/business/residents/presentation/
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon