@sugardarius/anzen

What is Anzen?

Fast, flexible, framework validation agnostic, type‑safe factories for creating server actions, route handlers, page and layout Server Component files in Next.js.

Why?

Anzen means safe in Japanese.

Without a shared pattern, server actions, route handlers, and page or layout Server Components often accumulate one-off checks, inconsistent error handling, and types that drift from runtime behavior. The examples below contrast plain Next.js with the same flows using Anzen — validation, authorization, and structured results in one place.

Before
actions/create-thread.ts
'use server'

import { auth } from '~/lib/auth'
import { db } from '~/lib/db'

export async function createThread(formData: FormData) {
  // Read fields from FormData — shape is implicit, not validated as one unit
  const spaceId = formData.get('spaceId')
  const commentRaw = formData.get('comment')

  // Ad-hoc guards; each failure path throws a different Error
  if (typeof spaceId !== 'string' || !spaceId) {
    throw new Error('Invalid spaceId')
  }
  if (typeof commentRaw !== 'string') {
    throw new Error('Invalid comment')
  }

  // Auth and domain logic live in the same function as parsing
  const session = await auth()
  if (!session.user) {
    throw new Error('unauthorized')
  }
  if (!session.access.includes(spaceId)) {
    throw new Error('forbidden')
  }

  // Nested JSON parsed and asserted by hand
  let comment: { createdAt: string; content: string }
  try {
    comment = JSON.parse(commentRaw) as { createdAt: string; content: string }
  } catch {
    throw new Error('Invalid comment JSON')
  }

  return db.createThread({
    thread: { spaceId, comment, authorId: session.user.id },
  })
}
After
actions/create-thread.ts
'use server'

import { object, string, datelike } from 'decoders'
import { createSafeServerAction } from '@sugardarius/anzen'

import { auth } from '~/lib/auth'
import { db } from '~/lib/db'

// Factory wires validation, auth, and handler with a stable result shape
export const createThread = createSafeServerAction(
  {
    id: 'create-thread-action',
    // One Standard Schema object — input is inferred + validated together
    input: object({
      spaceId: string,
      createdAt: datelike,
      comment: object({
        createdAt: datelike,
        content: string,
      }),
    }),
    // Authorization separated from the business handler
    authorize: async ({ input }) => {
      const session = await auth()
      if (!session.user) {
        throw new Error('user is not authenticated')
      }
      if (!session.access.includes(input.spaceId)) {
        throw new Error('user has not access')
      }
      return { user: session.user }
    },
  },
  // Typed `input` + `auth`; callers get `{ success, output | error }`
  async ({ auth, input, id }) => {
    const inserted = await db.createThread({
      thread: { ...input, authorId: auth.user.id },
    })
    return { inserted, actionId: id }
  }
)
Before
api/threads/route.ts
import { auth } from '~/lib/auth'

export async function POST(req: Request) {
  // Session check + early Response inlined in the handler
  const session = await auth.getSession(req)
  if (!session) {
    return new Response(null, { status: 401 })
  }

  // Body: manual parse + runtime type checks
  let body: unknown
  try {
    body = await req.json()
  } catch {
    return new Response('Invalid JSON', { status: 400 })
  }
  if (typeof body !== 'object' || body === null) {
    return new Response('Invalid body', { status: 400 })
  }

  return Response.json({ user: session.user, body }, { status: 200 })
}
After
api/threads/route.ts
import { createSafeRouteHandler } from '@sugardarius/anzen'
import { auth } from '~/lib/auth'

// Route is a factory — shared behavior (e.g. auth) lives in one place
export const POST = createSafeRouteHandler(
  {
    // Same 401 logic, but declared as a reusable authorize step
    authorize: async ({ req }) => {
      const session = await auth.getSession(req)
      if (!session) {
        return new Response(null, { status: 401 })
      }
      return { user: session.user }
    },
  },
  // `auth` is injected; handler focuses on the response
  async ({ auth, body }, req): Promise<Response> => {
    return Response.json({ user: auth.user, body }, { status: 200 })
  }
)
Before
app/[accountId]/page.tsx
import { unauthorized } from 'next/navigation'

import { auth } from '~/lib/auth'
import { getAccount } from '~/lib/db'

import { AccountSummary } from '~/components/account-summary'

type PageProps = { params: Promise<{ accountId: string }> }

export default async function Page({ params }: PageProps) {
  const { accountId } = await params

  // Segment shape is unchecked until you validate it by hand
  if (typeof accountId !== 'string' || !accountId) {
    unauthorized()
  }

  // Auth + data loading interleaved in the page body
  const session = await auth.getSession()
  if (!session) {
    unauthorized()
  }

  const account = await getAccount({ id: accountId })
  return <AccountSummary user={session.user} account={account} />
}
After
app/[accountId]/page.tsx
import { unauthorized } from 'next/navigation'
import { string } from 'decoders'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

import { auth } from '~/lib/auth'
import { getAccount } from '~/lib/db'

import { AccountSummary } from '~/components/account-summary'

// Segments + authorize + page body composed by the factory
export default createSafePageServerComponent(
  {
    authorize: async ({ segments }) => {
      const session = await auth.getSession()
      if (!session) {
        unauthorized()
      }

      return { user: session.user }
    },
    // Route params validated as one Standard Schema dictionary
    segments: {
      accountId: string,
    },
  },
  async ({ auth, segments }) => {
    const account = await getAccount({ id: segments.accountId })
    return <AccountSummary user={auth.user} account={account} />
  }
)
Before
app/[accountId]/layout.tsx
import type { ReactNode } from 'react'
import { unauthorized } from 'next/navigation'

import { auth } from '~/lib/auth'

import { AccountHeader } from '~/components/account-header'

type LayoutProps = {
  params: Promise<{ accountId: string }>
  children: ReactNode
}

export default async function Layout({ params, children }: LayoutProps) {
  const { accountId } = await params

  // Same manual segment checks repeated as in the page file
  if (typeof accountId !== 'string' || !accountId) {
    unauthorized()
  }

  const session = await auth.getSession()
  if (!session) {
    unauthorized()
  }

  return (
    <div>
      <AccountHeader user={session.user} accountId={accountId} />
      {children}
    </div>
  )
}
After
app/[accountId]/layout.tsx
import { unauthorized } from 'next/navigation'
import { string } from 'decoders'
import { createSafeLayoutServerComponent } from '@sugardarius/anzen/server-components'

import { auth } from '~/lib/auth'
import { getAccount } from '~/lib/db'

import { AccountHeader } from '~/components/account-header'

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

      return { user: session.user }
    },
    segments: {
      accountId: string,
    },
  },
  async ({ auth, segments, children }) => {
    const account = await getAccount({ id: segments.accountId })
    return (
      <div>
        <AccountHeader user={auth.user} accountId={segments.accountId} />
        {children}
      </div>
    )
  }
)

Philosophy

While server actions, route handlers, page, and layout Server Component files in Next.js are powerful features, they don't provide (yet) any means to validate the input data or authorize the access to the resource.

Anzen is a library providing factories to help you create safe server actions, route handlers, page and layout Server Component files in Next.js. It's a tool that helps you create code that is safe, flexible and easy to use.

It makes easier to build dynamic applications with proper data validation, authorization, and error handling in a structured way.

Features

🔧

Framework validation agnostic

Use a validation library supporting Standard Schema.

🧠

Focused functionalities

Use only the features you need.

🧹

Clean and flexible API

Make it your own.

🔒

Type-safe

Full TypeScript inference.

🌱

Dependency free

Only Next.js is required as a peer dependency.

🪶

Lightweight

Your bundle is safe.

Install

npm i @sugardarius/anzen

Usage

actions/create-thread.ts
  'use server'

  import { object, string, datelike } from 'decoders'
  import { createSafeServerAction } from '@sugardarius/anzen'

  import { auth } from '~/lib/auth'
  import { db } from '~/lib/db'

  export const createThread = createSafeServerAction(
    {
      id: 'create-thread-action',
      input: object({
        spaceId: string,
        createdAt: datelike,
        comment: object({
          createdAt: datelike,
          content: string,
        }),
      }),
      authorize: async ({ input }) => {
        const session = await auth()
        if (!session.user) {
          throw new Error('user is not authenticated')
        }

        if (!session.access.includes(input.spaceId)) {
          throw new Error('user has not access')
        }

        return { user: session.user }
      },
    },
    async ({ auth, input, id }) => {
      const inserted = await db.createThread({
        thread: { ...input, authorId: auth.user.id },
      })

      return { inserted, actionId: id }
    }
  )
api/authorize
  import { object, string, number } from 'decoders'
  import { createSafeRouteHandler } from '@sugardarius/anzen'
  import { auth } from '~/lib/auth'

  export const POST = createSafeRouteHandler(
    {
      authorize: async ({ req }) => {
        const session = await auth.getSession(req)
        if (!session) {
          return new Response(null, { status: 401 })
        }

        return { user: session.user }
      },
    },
    async ({ auth, body }, req): Promise<Response> => {
      return Response.json({ user: auth.user, body }, { status: 200 })
    }
  )
app/[accountId]/page.tsx
import { unauthorized } from 'next/navigation'
import { string } from 'decoders'
import { createSafePageServerComponent } from '@sugardarius/anzen/server-components'

import { auth } from '~/lib/auth'
import { getAccount } from '~/lib/db'

import { AccountSummary } from '~/components/account-summary'

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

      return { user: session.user }
    },
    segments: {
      accountId: string,
    },
  }, async ({ auth, segments })  => {
    const account = await getAccount({ id: segments.accountId})
    return <AccountSummary user={auth.user} account={account} />
  }
)
app/[accountId]/layout.tsx
import { unauthorized } from 'next/navigation'
import { string } from 'decoders'
import { createSafeLayoutServerComponent } from '@sugardarius/anzen/server-components'

import { auth } from '~/lib/auth'
import { getAccount } from '~/lib/db'

import { AccountHeader } from '~/components/account-header'

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

      return { user: session.user }
    },
    segments: {
      accountId: string,
    },
  }, async ({ auth, segments, children }) => {
    const account = await getAccount({ id: segments.accountId})
    return (
      <div>
        <AccountHeader user={auth.user} accountId={segments.accountId} />
        {children}
      </div>
    )
  }
)

When to use Anzen?

Use Anzen whenever you need to validate untrusted data, and/or authorize the access to the resource. It's a tool that helps you create code that is safe, flexible and easy to use. You should use it for:

  • ✅ Full-stack Next.js applications where you want a unified pattern across all server-side entry points
  • ✅ Applications requiring authorization at multiple levels (actions, routes, pages, layouts)
  • ✅ Projects needing consistent input validation across the entire stack
  • ✅ Type-safe applications where you want compile-time guarantees everywhere
  • ✅ Validation library flexibility — use Zod, Valibot, or any decoder you prefer
  • ✅ Clean error handling with structured responses throughout your app

Framework validation agnostic

All factories are framework validation agnostic. You can use whatever you want as framework validation as long as it implements the Standard Schema common interface. You can use your favorite validation library like decoders. Zod, or Validbot.

actions/update-profile.ts
'use server'

import { z } from 'zod'
import { object, string } from 'decoders'
import { createSafeServerAction } from '@sugardarius/anzen'

// Zod
export const updateProfile = createSafeServerAction(
  {
    input: z.object({
      displayName: z.string().min(1),
      bio: z.string().max(500).optional(),
    }),
  },
  async ({ input }) => ({ saved: input.displayName })
)

// decoders
export const renameSpace = createSafeServerAction(
  {
    input: object({
      spaceId: string,
      name: string,
    }),
  },
  async ({ input }) => ({ id: input.spaceId, name: input.name })
)

Synchronous validations

The Standard Schema contract discourages async validation. If a schema resolves validation asynchronously, behavior is undefined and may throw at runtime. So all Anzen's factories do not support async validations by design. An error will be thrown if you try to use async validation.

Learn more

API reference

Last updated on

On this page