Typed Fields
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.
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 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:
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:
const log = useLogger(event)
log.set({ anything: true, nested: { deep: 'value' } }) // OK
Typed fields are fully opt-in.
Nuxt Auto-Import
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.// 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
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 }
}
Next Steps
- Wide Events: Design effective wide events with context layering
- Best Practices: Security guidelines for preventing sensitive data leakage
- Configuration: All
initLoggerand middleware options