JSON.stringify for API route responses. Types like Date, Map, Set, and BigInt are not preserved across the wire by default. Date becomes an ISO string, Map and Set are dropped, and BigInt throws..page(), .layout(), .loader()), this is not a problem. React's flight protocol already handles Date, Map, Set, BigInt, typed arrays, Promise, and JSX elements natively..get(), .post(), .route()), you can add custom serialization using a response helper on the server and an onResponse hook on the fetch client. This page shows how to do it with superjson.12345678910// src/superjson.ts import superjson from 'superjson' export function superjsonResponse(data: any): Response { const { json, meta } = superjson.serialize(data) if (meta) (json as any).__superjsonMeta = meta return new Response(JSON.stringify(json), { headers: { 'content-type': 'application/superjson' }, }) }
1234567891011import { Spiceflow } from 'spiceflow' import { superjsonResponse } from './superjson' const app = new Spiceflow() .get('/api/event', () => superjsonResponse({ name: 'Launch party', date: new Date('2025-07-01T19:00:00Z'), attendees: new Set(['alice', 'bob']), }), )
__superjsonMeta field that records the original types.createSpiceflowFetch with an onResponse hook that checks for the custom content type and deserializes:1234567891011121314151617181920import superjson from 'superjson' import { createSpiceflowFetch } from 'spiceflow/client' const safeFetch = createSpiceflowFetch('http://localhost:3000', { onResponse: async (response) => { const ct = response.headers.get('content-type') if (!ct?.includes('application/superjson')) return undefined const data = await response.json() if (!data?.__superjsonMeta) return data const { __superjsonMeta, ...rest } = data return superjson.deserialize({ json: rest, meta: __superjsonMeta }) }, }) const event = await safeFetch('/api/event') if (event instanceof Error) throw event console.log(event.date) // Date object, not a string console.log(event.attendees) // Set {'alice', 'bob'}
onResponse returns a non-undefined value, it replaces the default response parsing entirely. Return undefined (or don't return) to fall through to the normal JSON parsing.1234567891011121314151617181920212223242526┌─────────────────────────────────────────────────────────────────────────────┐ │ Route handler │ │ │ │ superjsonResponse({ date: new Date(), ... }) │ │ │ │ │ ▼ │ │ superjson.serialize() ──▶ JSON with __superjsonMeta │ │ │ │ │ ▼ │ │ Response { content-type: application/superjson } │ └─────────────────────────────────────────────────────────────────────────────┘ │ HTTP wire │ ▼ ┌─────────────────────────────────────────────────────────────────────────────┐ │ onResponse hook │ │ │ │ detect content-type: application/superjson │ │ │ │ │ ▼ │ │ superjson.deserialize() ──▶ Date, Set, Map, BigInt restored │ │ │ │ │ ▼ │ │ return deserialized data (skips default JSON.parse) │ └─────────────────────────────────────────────────────────────────────────────┘
JSON.stringify, not after. That's why this uses a response helper in the handler rather than middleware. Middleware only sees the Response after the body has been stringified, so type information (Date vs string) is already lost.1234app.get('/api/event', () => ({ name: 'Launch party', date: new Date().toISOString(), // string on both sides }))
Date objects, Map, Set, BigInt, or other types that JSON.stringify doesn't preserve.