Typed Fields
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:
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
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
import { createWorkersLogger } from 'evlog/workers'
interface MyFields {
action: string
}
const log = createWorkersLogger<MyFields>(request)
log.set({ action: 'process' }) // OK
Design Tips
One Interface Per Domain
Define field interfaces per domain area, not per route:
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 }
}
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 }
}