npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

actium

v1.0.0

Published

Professional, minimal, and infinitely composable type-safe server actions for Next.js

Downloads

8

Readme

actium

Type-safe, composable server actions for Next.js

npm version License: MIT TypeScript Next.js


Overview

actium is a minimal library for building type-safe server actions in Next.js. It provides a composable, functional approach with perfect TypeScript inference, built-in validation, and comprehensive error handling.

Features

  • 🎯 Type-Safe - Perfect TypeScript inference across middleware chains
  • 🔗 Composable - Actions as middleware with infinite nesting support
  • ✅ Validation - Built-in Zod schema validation
  • ⚡ Minimal - Zero dependencies (peer deps: React, Zod)
  • 🎣 React Hook - Optimized useAction hook with loading states
  • 🛡️ Error Handling - Structured error handling with field-level errors
  • 📦 Tiny - < 5KB minified + gzipped

Installation

pnpm add actium zod
# or
npm install actium zod
# or
yarn add actium zod

Requirements: Next.js 14+, React 18+, Zod 3+, TypeScript 5+

Quick Start

1. Create a Server Action

// app/actions.ts
"use server";

import { createAction } from "actium";
import { z } from "zod";

export const createPost = createAction()
  .input(z.object({
    title: z.string().min(1),
    content: z.string(),
  }))
  .handler(async ({ input }) => {
    const post = await db.post.create({ data: input });
    return { post };
  });

2. Use in Client Component

// app/components/CreatePostForm.tsx
"use client";

import { useAction } from "actium/react";
import { createPost } from "../actions";

export function CreatePostForm() {
  const { execute, isPending, error, data } = useAction(createPost, {
    onSuccess: (data) => {
      console.log("Post created:", data.post);
    },
  });

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      execute({
        title: formData.get("title") as string,
        content: formData.get("content") as string,
      });
    }}>
      <input name="title" required />
      <textarea name="content" required />
      <button disabled={isPending}>
        {isPending ? "Creating..." : "Create Post"}
      </button>
      {error && <p className="error">{error.message}</p>}
    </form>
  );
}

Core Concepts

Composable Actions

Actions compose like functions. Each action can use other actions as middleware, with perfect type inference:

const actionA = createAction().handler(() => ({ a: 1 }));
const actionB = createAction().handler(() => ({ b: 2 }));

const actionC = createAction()
  .use(actionA)
  .use(actionB)
  .handler(({ ctx }) => ({
    total: ctx.a + ctx.b // ✅ Fully typed!
  }));

Middleware Chain

Middleware executes in order and merges context:

const auth = createAction().handler(async () => {
  const user = await getCurrentUser();
  if (!user) throw new ActionError("NOT_AUTHORIZED");
  return { user };
});

const myAction = createAction()
  .use(auth)
  .handler(({ ctx }) => {
    // ctx.user is available and typed!
    return { success: true };
  });

Examples

Authentication Middleware

// lib/middleware.ts
import { createAction, ActionError } from "actium";
import { cookies } from "next/headers";

export const auth = createAction().handler(async () => {
  const session = cookies().get("session");
  if (!session) {
    throw new ActionError("NOT_AUTHORIZED", "Please login");
  }
  
  const user = await getUserFromSession(session.value);
  return { user };
});

// Usage
export const deletePost = createAction()
  .input(z.object({ postId: z.string() }))
  .use(auth)
  .handler(async ({ input, ctx }) => {
    // ctx.user is available and typed
    const post = await db.post.findUnique({ 
      where: { id: input.postId } 
    });
    
    if (post.authorId !== ctx.user.id) {
      throw new ActionError("FORBIDDEN", "Not your post");
    }
    
    await db.post.delete({ where: { id: input.postId } });
    return { deleted: true };
  });

Role-Based Authorization

export const requireRole = (role: string) => 
  createAction()
    .use(auth)
    .handler(({ ctx }) => {
      if (ctx.user.role !== role) {
        throw new ActionError("FORBIDDEN", `Requires ${role} role`);
      }
      return {};
    });

export const adminAction = createAction()
  .use(requireRole("admin"))
  .handler(async ({ ctx }) => {
    // Only admins can execute this
    return { success: true };
  });

Error Handling

const { execute, error } = useAction(createPost, {
  onError: (error) => {
    if (error.code === "NOT_AUTHORIZED") {
      router.push("/login");
    } else if (error.code === "VALIDATION_ERROR") {
      // Field errors available in error.fieldErrors
      console.log(error.fieldErrors);
    }
  },
});

// Display field errors
{error?.fieldErrors?.title && (
  <span className="error">{error.fieldErrors.title[0]}</span>
)}

API Reference

createAction()

Creates a new action builder instance.

const action = createAction();

.input(schema)

Defines input validation using Zod schema. The input type is automatically inferred.

const action = createAction()
  .input(z.object({
    email: z.string().email(),
    age: z.number().min(18),
  }))
  .handler(async ({ input }) => {
    // input is typed as { email: string; age: number }
    return { success: true };
  });

.use(middleware | action)

Chains middleware or another action. Context types are automatically merged.

const middleware = createAction().handler(() => ({ userId: "123" }));

const action = createAction()
  .use(middleware)
  .handler(({ ctx }) => {
    // ctx.userId is available and typed
    return { success: true };
  });

.handler(fn)

Defines the action handler function. Returns a callable server action.

Parameters:

  • input: Validated input (if schema defined)
  • ctx: Merged context from all middleware
const action = createAction()
  .input(z.object({ name: z.string() }))
  .handler(async ({ input, ctx }) => {
    return { message: `Hello ${input.name}` };
  });

useAction(action, options)

React hook for executing server actions with state management.

Options:

type UseActionOptions<TData> = {
  onSuccess?: (data: TData) => void | Promise<void>;
  onError?: (error: ActionErrorResponse, retryCount?: number) => void | Promise<void>;
  onSettled?: (data: TData | undefined, error: ActionErrorResponse | undefined) => void | Promise<void>;
  retry?: number;        // Number of retry attempts (default: 0)
  retryDelay?: number;   // Delay between retries in ms (default: 1000)
};

Returns:

type UseActionReturn<TInput, TData> = {
  execute: (input: TInput) => void;           // Fire and forget
  executeAsync: (input: TInput) => Promise<TData>;  // Returns promise
  reset: () => void;                          // Reset state
  isPending: boolean;                         // Loading state
  data: TData | undefined;                    // Success data
  error: ActionErrorResponse | undefined;     // Error object
  isSuccess: boolean;                         // Success flag
  isError: boolean;                           // Error flag
};

ActionError

Custom error class for action errors.

import { ActionError } from "actium";

throw new ActionError(
  "NOT_AUTHORIZED",     // Error code
  "Login required",     // Error message
  { email: ["Invalid email"] }  // Optional field errors
);

Error Codes:

  • NOT_AUTHORIZED - User not authenticated
  • FORBIDDEN - User lacks permission
  • VALIDATION_ERROR - Input validation failed
  • ERROR - Generic error

Best Practices

1. Organize Actions by Feature

app/
├── actions/
│   ├── auth.ts          # Authentication actions
│   ├── posts.ts         # Post-related actions
│   ├── users.ts         # User-related actions
│   └── middleware.ts    # Shared middleware

2. Create Reusable Middleware

// app/actions/middleware.ts
export const auth = createAction().handler(async () => {
  const session = cookies().get("session");
  if (!session) throw new ActionError("NOT_AUTHORIZED");
  
  const user = await getUser(session.value);
  return { user };
});

export const adminOnly = createAction()
  .use(auth)
  .handler(({ ctx }) => {
    if (ctx.user.role !== "admin") {
      throw new ActionError("FORBIDDEN", "Admin access required");
    }
    return {};
  });

3. Validate Input Strictly

const createPost = createAction()
  .input(z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(10),
    tags: z.array(z.string()).max(5),
    published: z.boolean().default(false),
  }))
  .handler(async ({ input }) => {
    // input is strictly validated
  });

Troubleshooting

"use server" directive missing

Make sure your action files have "use server" at the top:

"use server";
import { createAction } from "actium";

Type inference not working

Ensure you're using TypeScript 5+ with strict mode enabled in tsconfig.json.

Middleware context not available

Make sure middleware is added before the handler:

// ✅ Correct
const action = createAction()
  .use(middleware)
  .handler(({ ctx }) => { /* ctx available */ });

FAQ

Can I use actium without React?

Yes! The core library works without React. Only import actium/react if you need the useAction hook.

Does actium work with the Pages Router?

actium is designed for the App Router with Server Actions. For the Pages Router, consider using API routes.

How do I handle file uploads?

Use FormData as input:

export const uploadFile = createAction()
  .handler(async ({ input }) => {
    const formData = input as FormData;
    const file = formData.get("file") as File;
    // Process file...
    return { uploaded: true };
  });

Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Setup

git clone https://github.com/ogabekyuldoshev/actium.git
cd actium
pnpm install
pnpm typecheck
pnpm lint
pnpm build

License

MIT License - see LICENSE file for details

Copyright (c) 2026 Ogabek Yuldoshev


DocumentationIssuesDiscussions

Made with ❤️ for the Next.js community