fatline

Next.js

Server Components, API routes, and Server Actions

Next.js serializes data when passing from Server Components to Client Components. By default, this means Date, Set, and Map become strings and arrays.

The Problem

// Server Component
export default async function Page() {
  const user = { createdAt: new Date(), roles: new Set(['admin']) }
  return <ClientComponent user={user} />  // ← loses types
}

// Client Component
'use client'
export function ClientComponent({ user }) {
  user.createdAt instanceof Date  // false
  user.roles instanceof Set       // false
}

Solution 1: Manual Serialize/Deserialize

The simplest approach—serialize before passing, deserialize after:

// Server Component
import { serialize } from 'fatline'

export default async function Page() {
  const user = await getUser()
  return <ClientComponent userJson={serialize(user)} />
}
// Client Component
'use client'
import { deserialize } from 'fatline'

export function ClientComponent({ userJson }: { userJson: string }) {
  const user = deserialize(userJson)
  user.createdAt instanceof Date  // true
  user.roles instanceof Set       // true
}

Solution 2: API Routes

For client-side fetching, return serialized data from API routes:

// app/api/user/route.ts
import { serialize } from 'fatline'

export async function GET() {
  const user = await getUser()
  return new Response(serialize(user), {
    headers: { 'Content-Type': 'application/json' },
  })
}
// Client Component
'use client'
import { deserialize } from 'fatline'

export function UserProfile() {
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch('/api/user')
      .then(r => r.text())
      .then(json => setUser(deserialize(json)))
  }, [])
}

Solution 3: Server Actions

Server Actions can return rich types directly:

// actions.ts
'use server'
import { serialize } from 'fatline'

export async function getUser() {
  const user = await db.user.findUnique({ ... })
  return serialize(user)
}
// Client Component
'use client'
import { deserialize } from 'fatline'
import { getUser } from './actions'

export function UserButton() {
  async function handleClick() {
    const json = await getUser()
    const user = deserialize(json)
    // user.createdAt instanceof Date ✓
  }
}

With tRPC

If you're using tRPC with Next.js, the fatline transformer handles everything:

import { transformer } from 'fatline/trpc'

const t = initTRPC.create({ transformer })

No manual serialize/deserialize needed—your types just work.

With TanStack Query

See TanStack Query for SSR hydration setup.

Type Safety

For TypeScript, define your serialized types:

// types.ts
export interface User {
  id: string
  createdAt: Date
  roles: Set<string>
}

// Serialized form (what crosses the wire)
export type SerializedUser = string
// Server Component
import { serialize } from 'fatline'
import type { User, SerializedUser } from './types'

export default async function Page() {
  const user: User = await getUser()
  const userJson: SerializedUser = serialize(user)
  return <ClientComponent userJson={userJson} />
}

Next Steps

On this page