Core Concepts

Typed Fields

Add compile-time type safety to your wide events. Prevent typos and ensure consistent field names across your codebase.

By default, useLogger accepts any fields — great for getting started. But as your codebase grows, inconsistencies creep in: one route logs user, another logs account, a third logs userId. Typed fields solve this with opt-in compile-time safety.

Basic Usage

Define an interface for your fields and pass it as a generic to useLogger:

server/api/checkout.post.ts
import { useLogger } from 'evlog'

interface CheckoutFields {
  user: { id: string; plan: string }
  cart: { items: number; total: number }
  action: string
}

export default defineEventHandler(async (event) => {
  const log = useLogger<CheckoutFields>(event)

  log.set({ user: { id: '123', plan: 'pro' } })  // OK
  log.set({ cart: { items: 3, total: 9999 } })    // OK
  log.set({ action: 'checkout' })                  // OK

  log.set({ account: '...' })                      // TS error
  log.set({ usr: { id: '123' } })                  // TS error

  return { success: true }
})

TypeScript catches typos and unknown fields at compile time, before they reach production.

Internal Fields

evlog sets some fields internally (status, service). These are always accepted regardless of your type, through the InternalFields type:

log.set({ status: 200 })    // OK — internal field
log.set({ service: 'api' }) // OK — internal field

You don't need to include status or service in your interface.

Untyped Usage

Without a generic, useLogger accepts any fields — nothing changes from the default behavior:

const log = useLogger(event)
log.set({ anything: true, nested: { deep: 'value' } }) // OK

Typed fields are fully opt-in.

Nuxt Auto-Import

When using typed fields with useLogger<T>, you must use an explicit import. The Nuxt auto-import does not support excess property checking for generics due to a TypeScript limitation.
// Works — explicit import preserves type checking
import { useLogger } from 'evlog'
const log = useLogger<MyFields>(event)
log.set({ typo: 'oops' }) // TS error

// Does NOT work — auto-import loses excess property checking
const log = useLogger<MyFields>(event)
log.set({ typo: 'oops' }) // No error (silently accepted)

The auto-import works perfectly for untyped usage. Only add the explicit import when you need typed fields.

Outside Nuxt

The same generic works with createRequestLogger and createWorkersLogger:

import { createRequestLogger } from 'evlog'

interface MyFields {
  action: string
  userId: string
}

const log = createRequestLogger<MyFields>({
  method: 'POST',
  path: '/checkout',
})

log.set({ action: 'checkout', userId: '123' }) // OK
log.set({ unknown: true })                      // TS error

Design Tips

One Interface Per Domain

Define field interfaces per domain area, not per route:

server/types/log-fields.ts
export interface AuthFields {
  user: { id: string; email: string; role: string }
  action: string
  mfaUsed: boolean
}

export interface PaymentFields {
  user: { id: string; plan: string }
  order: { id: string; total: number; currency: string }
  payment: { method: string; last4: string }
}
server/api/auth/login.post.ts
import { useLogger } from 'evlog'
import type { AuthFields } from '~/server/types/log-fields'

export default defineEventHandler(async (event) => {
  const log = useLogger<AuthFields>(event)
  // ...
})

Keep Interfaces Focused

Include only the fields your routes actually set. The interface doesn't need to mirror your entire data model:

// Too broad — most routes won't set all these
interface EverythingFields {
  user: FullUserProfile
  order: CompleteOrder
  payment: PaymentDetails
  shipping: ShippingInfo
}

// Focused — only what this route sets
interface CheckoutFields {
  user: { id: string; plan: string }
  cart: { items: number; total: number }
}
Copyright © 2026