TanStack Start
Practical patterns for using evlog with TanStack Start. TanStack Start uses Nitro v3 as its server layer, so evlog integrates via the evlog/nitro/v3 module.
Setup
Starting from a TanStack Start project created with npm create @tanstack/start@latest:
1. Install evlog
npm install evlog
2. Add nitro.config.ts
Create a nitro.config.ts at the project root to register the evlog module. Your vite.config.ts already has the nitro() plugin from the CLI — no changes needed there.
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
experimental: {
asyncContext: true,
},
modules: [
evlog({
env: { service: 'my-app' },
}),
],
})
Enabling asyncContext lets you access the request-scoped logger from anywhere in the call stack via useRequest().
3. Error handling middleware
TanStack Start has its own error handling layer that runs before Nitro's. To ensure throw createError() returns a proper JSON response with why, fix, and link, add the evlogErrorHandler middleware to your root route:
import { createRootRoute } from '@tanstack/react-router'
import { createMiddleware } from '@tanstack/react-start'
import { evlogErrorHandler } from 'evlog/nitro/v3'
export const Route = createRootRoute({
server: {
middleware: [createMiddleware().server(evlogErrorHandler)],
},
// ... head, shellComponent, etc.
})
That's it. evlog automatically captures every request as a wide event with method, path, status, and duration.
Wide Events
With experimental.asyncContext: true, use useRequest() from nitro/context to access the request-scoped logger and build up context progressively:
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import type { RequestLogger } from 'evlog'
export const Route = createFileRoute('/api/hello')({
server: {
handlers: {
GET: async () => {
const req = useRequest()
const log = req.context.log as RequestLogger
log.set({ user: { id: 'user_123', plan: 'pro' } })
log.set({ action: 'fetch_profile' })
log.set({ cache: { hit: true, ttl: 3600 } })
return Response.json({ ok: true })
},
},
},
})
All fields are merged into a single wide event emitted when the request completes:
14:58:15 INFO [my-app] GET /api/hello 200 in 52ms
├─ cache: hit=true ttl=3600
├─ action: fetch_profile
├─ user: id=user_123 plan=pro
└─ requestId: 4a8ff3a8-...
useRequest() is an experimental Nitro v3 feature powered by AsyncLocalStorage. It works on Node.js and Bun runtimes.Error Handling
Use createError for structured errors with why, fix, and link fields:
import { createFileRoute } from '@tanstack/react-router'
import { useRequest } from 'nitro/context'
import { createError } from 'evlog'
import type { RequestLogger } from 'evlog'
export const Route = createFileRoute('/api/checkout')({
server: {
handlers: {
POST: async ({ request }) => {
const req = useRequest()
const log = req.context.log as RequestLogger
const body = await request.json()
log.set({ user: { id: body.userId, plan: body.plan } })
log.set({ cart: { items: body.items, total: body.total } })
const result = await chargeCard(body)
if (!result.success) {
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
}
return Response.json({ success: true, orderId: result.orderId })
},
},
},
})
The error is captured and logged with both the custom context and structured error fields:
14:58:20 ERROR [my-app] POST /api/checkout 402 in 104ms
├─ error: name=EvlogError message=Payment failed status=402
├─ cart: items=3 total=9999
├─ user: id=user_123 plan=pro
└─ requestId: 880a50ac-...
Parsing Errors on the Client
Use parseError to extract the structured fields from any error response:
import { parseError } from 'evlog'
try {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ userId: 'user_123' }),
})
if (!res.ok) throw { data: await res.json(), status: res.status }
} catch (error) {
const { message, status, why, fix, link } = parseError(error)
}
Drain & Enrichers
Since TanStack Start uses Nitro v3, configure drains and enrichers via Nitro plugins. Create a server/plugins/ directory and register hooks:
import { definePlugin } from 'nitro'
import { createAxiomDrain } from 'evlog/axiom'
export default definePlugin((nitroApp) => {
const axiom = createAxiomDrain()
nitroApp.hooks.hook('evlog:drain', axiom)
})
import { definePlugin } from 'nitro'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
export default definePlugin((nitroApp) => {
const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
for (const enricher of enrichers) enricher(ctx)
})
})
Run Locally
git clone https://github.com/HugoRCD/evlog.git
cd evlog/examples/tanstack-start
bun install
bun run dev
Open http://localhost:3000 and navigate to the evlog Demo page to test the API endpoints.