@sugardarius/anzen

Safe page server component

API reference for the safe App Router page factory — validated segments and search params, authorization, and error handling.

createSafePageServerComponent wraps a default-exported page Server Component. It validates dynamic segments and search params with Standard Schema dictionaries, runs optional authorization, and exposes a typed ctx to your render function. Import from the @sugardarius/anzen/server-components entry.

Import

import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

Signature

function createSafePageServerComponent<
  AC = undefined,
  TSegments = undefined,
  TSearchParams = undefined,
>(
  options: CreateSafePageServerComponentOptions<AC, TSegments, TSearchParams>,
  page: (
    ctx: SafePageServerComponentContext<AC, TSegments, TSearchParams>
  ) => Promise<React.ReactElement>
): (props: {
  params: Awaitable<any>
  searchParams: Awaitable<any>
}) => Promise<React.ReactElement>

The returned function is what you export default from page.tsx.

Options

When creating a safe page server component you can use a bunch of options for helping you achieve different tasks 👇🏻

id

id?: string

Used for logging in development or when the debug option is enabled. You can also use it to add extra logging or monitoring. By default the id is set to [unknown:page:server:component].

app/races/[id]/page.tsx
export default createSafePageServerComponent(
  {
    id: 'races/[id]/page',
  },
  async ({ id }) => {
    return <div>Page {id}</div>
  }
)

onError

onError?: (err: unknown) => Awaitable<never>

Callback triggered when the pageserver component throws an unhandled error. By default it rethrows the error as a service hatch to Next.js do its job and use error boundaries. The error is logged into the console. Use it if you want to manage unexpected errors properly to log, trace or define behaviors like using notFound or redirect.

app/page.tsx
import { notFound } from 'next/navigation'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

export default createSafePageServerComponent(
  {
    onError: async (err: unknown): Promise<never> => {
      if (err instanceof NotFoundError) {
        notFound()
      }
      throw err
    },
  },
  async () => {
    return <div>Hello</div>
  }
)

debug

debug?: boolean

Use this options to enable debug mode. It will add logs in the page server component to help you debug the request. By default it's set to false for production builds. In development builds, it will be true if NODE_ENV is not set to production.

app/page.tsx
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

export default createSafePageServerComponent({ debug: true }, async () => {
  return <div>Hello</div>
})

You can configure page server component options' validation using a validation library dynamic route segments and URL query parameters 👇🏻

authorize

authorize?: PageAuthFunction<AC, TSegments, TSearchParams>

Function to use to authorize the page server component. By default it always authorize the server component.

Return never (throws an error, notFound, forbidden, unauthorized, or redirect) when the request to the server component is not authorized.

The authorize function receives validated attributes (segments, searchParams) when they are defined, allowing you to use validated data for authorization logic.

Parameters:

  • id: string - Server component ID
  • segments?: - Validated route dynamic segments (if segments option is defined)
  • searchParams?: - Validated search params (if searchParams option is defined)
Basic authorization
app/page.tsx
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { auth } from '~/lib/auth'
import { unauthorized } from 'next/navigation'

export default createSafePageServerComponent(
  {
    authorize: async () => {
      const session = await auth.getSession()
      if (!session) {
        unauthorized()
      }

      return { user: session.user }
    },
  },
  async ({ auth }) => {
    return <div>Hello {auth.user.name}!</div>
  }
)
Authorization with validated segments
app/accounts/[accountId]/projects/[projectId]/page.tsx
import { z } from 'zod'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { auth } from '~/lib/auth'
import { notFound } from 'next/navigation'

export default createSafePageServerComponent(
  {
    segments: {
      accountId: z.string(),
      projectId: z.string(),
    },
    authorize: async ({ segments }) => {
      // segments are already validated at this point
      const session = await auth.getSession()
      if (!session) {
        throw new Error('Unauthorized')
      }

      // Check if user has access to this account
      const hasAccess = await checkAccountAccess(
        session.user.id,
        segments.accountId
      )
      if (!hasAccess) {
        notFound()
      }

      return { user: session.user }
    },
  },
  async ({ auth, segments }) => {
    return (
      <div>
        {auth.user.name} - {segments.accountId}/{segments.projectId}
      </div>
    )
  }
)
Authorization with validated search params
app/page.tsx
import { z } from 'zod'
import { unauthorized } from 'next/navigation'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

export default createSafePageServerComponent(
  {
    searchParams: {
      code: z.string(),
    },
    authorize: async ({ searchParams }) => {
      // searchParams are already validated at this point
      const isValidCode = await validateCode(searchParams.code)
      if (!isValidCode) {
        unauthorized()
      }

      return { code: searchParams.code }
    },
  },
  async ({ auth, searchParams }) => {
    return <div>Code: {auth.code}</div>
  }
)
Authorization with all validated props
app/accounts/[accountId]/page.tsx
import { z } from 'zod'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { auth } from '~/lib/auth'
import { notFound } from 'next/navigation'

export default createSafePageServerComponent(
  {
    segments: { accountId: z.string() },
    searchParams: { role: z.string() },
    authorize: async ({ segments, searchParams }) => {
      // All props are validated and available
      const session = await auth.getSession()
      if (!session) {
        throw new Error('Unauthorized')
      }

      const hasPermission = await checkPermission(
        session.user.id,
        segments.accountId,
        searchParams.role
      )
      if (!hasPermission) {
        notFound()
      }

      return { user: session.user }
    },
  },
  async ({ auth, segments, searchParams }) => {
    return (
      <div>
        {auth.user.name} - {segments.accountId} - {searchParams.role}
      </div>
    )
  }
)

segments

segments?: TSegments

Dynamic route segments used for the page server component path. By design it will handle automatically if the segments are a Promise or not.

Please note the expected input is a StandardSchemaDictionary.

app/accounts/[accountId]/projects/[projectId]/page.tsx
import { z } from 'zod'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

export default createSafePageServerComponent(
  {
    segments: {
      accountId: z.string(),
      projectId: z.string().optional(),
    },
  },
  async ({ segments }) => {
    return (
      <div>
        Account: {segments.accountId} - Project: {segments.projectId}
      </div>
    )
  }
)

onSegmentsValidationError

onSegmentsValidationError?: OnValidationError

Callback triggered when dynamic segments validations returned issues. By default it throws a ValidationError and issues are logged into the console.

app/accounts/[accountId]/projects/[projectId]/page.tsx
import { z } from 'zod'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { notFound } from 'next/navigation'

export default createSafePageServerComponent(
  {
    segments: {
      accountId: z.string(),
      projectId: z.string().optional(),
    },
    onSegmentsValidationError: async (issues) => {
      console.error('Invalid segments', issues)
      notFound()
    },
  },
  async ({ segments }) => {
    return (
      <div>
        Account: {segments.accountId} - Project: {segments.projectId}
      </div>
    )
  }
)

searchParams

searchParams?: TSearchParams

Search params used in the page.

Please note the expected input is a StandardSchemaDictionary.

app/search/page.tsx
import { string, numeric, optional } from 'decoders'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

export default createSafePageServerComponent(
  {
    searchParams: {
      query: string,
      page: optional(numeric),
    },
  },
  async ({ searchParams }) => {
    return (
      <div>
        Query: {searchParams.query} - Page: {searchParams.page}
      </div>
    )
  }
)

onSearchParamsValidationError

onSearchParamsValidationError?: OnValidationError

Callback triggered when search params validations returned issues. By default it throws a ValidationError and issues are logged into the console.

app/search/page.tsx
import { string, numeric, optional } from 'decoders'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { redirect } from 'next/navigation'

export default createSafePageServerComponent(
  {
    searchParams: {
      query: string,
      page: optional(numeric),
    },
    onSearchParamsValidationError: async (issues) => {
      console.error('Invalid search params', issues)
      redirect('/')
    },
  },
  async ({ searchParams }) => {
    return (
      <div>
        Query: {searchParams.query} - Page: {searchParams.page}
      </div>
    )
  }
)

Handler context

SafePageServerComponentContext is the context object that is passed to the handler

FieldWhen
idAlways.
authWhen authorize returned a value.
segmentsWhen segments was configured.
searchParamsWhen searchParams was configured.

There is no children on the page context (use the layout factory for that).

Error handling

By design the factory will catch any error thrown in the page server component and rethrow it by default to let Next.js handle it with error boundaries.

You can customize the error handling if you want to fine tune error management.

app/page.tsx
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'
import { NotFoundError, DbUnknownError } from '~/lib/errors'
import { db } from '~/lib/db'
import { notFound } from 'next/navigation'

export default createSafePageServerComponent(
  {
    onError: async (err: unknown): Promise<never> => {
      // Take it as an example not as a real use case 😅
      if (err instanceof NotFoundError) {
        notFound()
      } else if (err instanceof DbUnknownError) {
        console.error('Database error', err)
        throw err
      }

      throw err
    },
  },
  async () => {
    const [data, err] = await db.findUnique({ id: 'liveblocks' })

    if (err) {
      throw new DbUnknownError(err.message, 500)
    }

    if (data === null) {
      throw new NotFoundError()
    }

    return <div>{data.name}</div>
  }
)

Synchronous Validation

Validation must be synchronous. The Standard Schema contract discourages async validation. If a schema resolves validation asynchronously, behavior is undefined and may throw at runtime.

Fair use note

Please note that if you're not using any of the proposed options in createSafePageServerComponent it means you're surely don't need it.

app/page.tsx
// Calling 👇🏻
export default createSafePageServerComponent({}, async () => {
  return <div>Hello</div>
})

// is equal to declare the page server component this way 👇🏻
export default async function Page() {
  return <div>Hello</div>
}

See also

Last updated on

On this page