@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 (or safety) 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 }
  },
)

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 }
    }
  )

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