Skip to content

Server Composable Patterns

Design patterns for generated server-side composables.

Server Composable Structure

Basic Pattern

typescript
export async function getServerPet(
  event: H3Event,
  id: number,
  options?: ServerComposableOptions<Pet>
): Promise<Pet> {
  return await $fetch<Pet>(`/pets/${id}`, {
    baseURL: useRuntimeConfig().apiBase,
    headers: getProxyHeaders(event)
  })
}

HTTP Methods

typescript
// GET
export async function getServerPet(
  event: H3Event,
  id: number
): Promise<Pet>

// POST
export async function createServerPet(
  event: H3Event,
  body: CreatePetRequest
): Promise<Pet>

// PUT
export async function updateServerPet(
  event: H3Event,
  id: number,
  body: UpdatePetRequest
): Promise<Pet>

// DELETE
export async function deleteServerPet(
  event: H3Event,
  id: number
): Promise<void>

BFF Route Patterns

Simple Proxy

typescript
// server/api/pets/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  return await getServerPet(event, Number(id))
})

With Transformation

typescript
// server/api/pets/[id].get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  const pet = await getServerPet(event, Number(id))
  
  return {
    ...pet,
    canEdit: event.context.user?.id === pet.ownerId
  }
})

Aggregate Multiple Sources

typescript
// server/api/pets/[id]/full.get.ts
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id')
  
  const [pet, owner] = await Promise.all([
    getServerPet(event, Number(id)),
    getServerUser(event, pet.ownerId)
  ])
  
  return { pet, owner }
})

Authentication Patterns

Header Forwarding

typescript
export async function getServerPet(event: H3Event, id: number) {
  return await $fetch(`/pets/${id}`, {
    headers: getProxyHeaders(event, {
      include: ['authorization', 'cookie']
    })
  })
}

Token Injection

typescript
export async function getServerPet(event: H3Event, id: number) {
  const token = getRequestHeader(event, 'authorization')
  
  return await $fetch(`/pets/${id}`, {
    headers: {
      Authorization: token || ''
    }
  })
}

Auth Context

typescript
// server/api/pets/index.post.ts
export default defineEventHandler(async (event) => {
  const auth = requireAuth(event)
  
  const body = await readBody(event)
  return await createServerPet(event, {
    ...body,
    ownerId: auth.userId
  })
})

Error Handling Patterns

Basic Error Handling

typescript
export default defineEventHandler(async (event) => {
  try {
    const pet = await getServerPet(event, 1)
    return pet
  } catch (error: any) {
    throw createError({
      statusCode: error.statusCode || 500,
      message: error.message
    })
  }
})

Error Transformation

typescript
export default defineEventHandler(async (event) => {
  try {
    return await getServerPet(event, 1)
  } catch (error: any) {
    if (error.statusCode === 404) {
      throw createError({
        statusCode: 404,
        message: 'Pet not found'
      })
    }
    throw error
  }
})

Transformation Patterns

Add Permissions

typescript
export default defineEventHandler(async (event) => {
  const pet = await getServerPet(event, 1)
  const user = event.context.user
  
  return {
    ...pet,
    canEdit: user?.id === pet.ownerId,
    canDelete: user?.role === 'admin'
  }
})

Filter Sensitive Data

typescript
export default defineEventHandler(async (event) => {
  const user = await getServerUser(event, 1)
  
  const { password, ssn, ...publicData } = user
  return publicData
})

Combine Sources

typescript
export default defineEventHandler(async (event) => {
  const pets = await getServerPets(event)
  
  // Fetch all owners in parallel
  const ownerIds = [...new Set(pets.map(p => p.ownerId))]
  const owners = await Promise.all(
    ownerIds.map(id => getServerUser(event, id))
  )
  
  const ownerMap = new Map(owners.map(o => [o.id, o]))
  
  return pets.map(pet => ({
    ...pet,
    owner: ownerMap.get(pet.ownerId)
  }))
})

Caching Patterns

Route Caching

typescript
export default defineCachedEventHandler(
  async (event) => {
    return await getServerPets(event)
  },
  {
    maxAge: 60 * 5 // 5 minutes
  }
)

Conditional Caching

typescript
export default defineCachedEventHandler(
  async (event) => {
    const auth = event.context.auth
    
    // Don't cache authenticated requests
    if (auth) {
      return await getServerPets(event)
    }
    
    return await getServerPets(event)
  },
  {
    maxAge: 60,
    getKey: (event) => {
      const auth = event.context.auth
      return auth ? `pets-${auth.userId}` : 'pets-public'
    }
  }
)

Query Parameter Patterns

Forward Query Params

typescript
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  
  return await getServerPets(event, {
    status: query.status,
    limit: Number(query.limit) || 10
  })
})

Validate Query Params

typescript
export default defineEventHandler(async (event) => {
  const query = getQuery(event)
  
  const status = query.status
  if (status && !['available', 'pending', 'sold'].includes(status)) {
    throw createError({
      statusCode: 400,
      message: 'Invalid status'
    })
  }
  
  return await getServerPets(event, { status })
})

Next Steps

Released under the Apache-2.0 License.