Learn

Typed Fields

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

By default, useLogger accepts any fields, which is 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.

checkout.post.ts·EDITING
1import { useLogger } from 'evlog'
2interface CheckoutFields {
user: { id: string; plan: string }
cart: { items: number; total: number }
action: string
6}
8const log = useLogger<CheckoutFields>(event)
10
11
problems · 0tsserver
No type errors. Excess properties on the literal would be flagged here.
useLogger<CheckoutFields>(event)
Excess property checking happens at the literal — autocomplete only suggests known keys.

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 then catches a typo or an unknown field at compile time, which is the only place a field-name mistake is cheap: once the event is in your drain the wrong key is already indexed, already queried, and already in someone's dashboard.

Internal Fields

Some fields evlog sets itself. status and service are always accepted whatever your type says, through InternalFields:

server/api/checkout.post.ts
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 as usual:

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

Typed fields are fully opt-in.

Nuxt Auto-Import

Typed fields with useLogger<T> need an explicit import. The auto-import cannot carry excess property checking through a generic, a TypeScript limitation rather than a module one.
server/api/checkout.post.ts
// 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)

Untyped usage keeps the auto-import. Add the explicit one only where you pass a generic.

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:

server/types/evlog.ts
// 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 }
}

Next Steps