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

@fy-tools/rpc-client-nextjs

v0.0.120

Published

Next.js integration for `@fy-tools/rpc-client`. Provides a server-side API factory for Next.js Server Actions and React hooks (`useFetcher`, `useMutation`) for client components — all with full type inference from your schema.

Readme

@fy-tools/rpc-client-nextjs

Next.js integration for @fy-tools/rpc-client. Provides a server-side API factory for Next.js Server Actions and React hooks (useFetcher, useMutation) for client components — all with full type inference from your schema.

Installation

npm install @fy-tools/rpc-client-nextjs @fy-tools/rpc-client

Overview

This package has three entry points:

| Entry point | Usage | |---|---| | @fy-tools/rpc-client-nextjs/server | createApiFactory — used in Server Actions | | @fy-tools/rpc-client-nextjs/client | useFetcher, useMutation — used in Client Components | | @fy-tools/rpc-client-nextjs | Type exports only (ApiResponse, etc.) |


Server Actions

createApiFactory(options?)

Creates a factory for building typed server action wrappers around rpc-client route functions. Handles authentication tokens, cache revalidation, and error normalization.

// app/_api/auth.request.ts
'use server';

import type { Schema } from '@app/schema';
import { InferError, rpcClient } from '@fy-tools/rpc-client';
import { createApiFactory } from '@fy-tools/rpc-client-nextjs/server';
import { redirect } from 'next/navigation';

import { getAuthToken, setAuthToken } from '@/util/auth';

const client = rpcClient<Schema>({ baseURL: process.env.BACKEND_BASE_URL });

async function refreshToken() {
  const token = await getAuthToken();
  if (!token.access && token.refresh) {
    const res = await client.auth.post_refresh__token({
      body: { username: token.username, refreshToken: token.refresh },
    });
    if (res.data.nextStep === 'DONE') {
      await setAuthToken(res.data.authToken);
    }
  }
  if (!(await getAuthToken()).access) redirect('/login');
}

const { createApi } = createApiFactory<InferError<Schema>>({
  getAccessToken: async () => (await getAuthToken()).access,
  refreshToken,
  onAuthorized: () => redirect('/login'),
});

export const login = createApi(client.auth.post_login);
export type login = typeof login.$infer;

export const getUsers = createApi(client.users.get_default);
export type getUsers = typeof getUsers.$infer;

export const createUser = createApi(client.users.post_default, {
  isMutation: true,
  tags: ['users'],
});
export type createUser = typeof createUser.$infer;

createApiFactory options (ClientFactoryPayload)

| Option | Type | Description | |---|---|---| | getAccessToken | () => Promise<string \| undefined> \| string \| undefined | Returns the current access token | | refreshToken | () => Promise<void> \| void | Called when no access token is found; should attempt to refresh | | onAuthorized | () => Promise<void> \| void | Called when no access token remains after refresh (e.g. redirect to login) |

createApi(fn, options?)

Wraps a rpc-client route function into a server action with auth headers, error handling, and cache control.

const { createApi } = createApiFactory({ ... });

export const deleteUser = createApi(client.users.delete_$id, {
  isMutation: true,
  tags: ['users'],
  postRequest: async (response, payload) => {
    // runs after a successful (non-error) response
    console.log('Deleted user', payload.params.id);
  },
});

createApi options (ApiOptions)

| Option | Type | Default | Description | |---|---|---|---| | authorized | false | — | Set to false to skip auth header injection | | isMutation | true | — | Triggers revalidateTag for all listed tags after the call | | isFileDownload | boolean | false | Sets Axios responseType: 'blob' for file downloads | | tags | string[] | [] | Next.js cache tags to attach (and revalidate if isMutation) | | postRequest | (response, payload) => void | — | Hook called after a successful response |

Return value & types

The wrapped function returns Promise<ApiResponse<T>> where T is inferred from the route's .response() schema.

const result = await login({ body: { username: 'alice', password: 'secret' } });

if ('error' in result) {
  console.error(result.error); // { status, statusText, data, headers }
} else {
  console.log(result.access); // typed as the route's response
}

Export both the function and its type so client components can infer payload and response types:

export const login = createApi(client.auth.post_login);
export type login = typeof login.$infer;
//                              ^^^^^^ provides Body, Params, Query, Response

Client Components

Import hooks from the /client entry point.

useFetcher(key, apiMethod, args, options?, config?)

SWR hook for fetching data. Calls the server action on mount and returns data, isLoading, error, and a mutate function.

'use client';

import { useFetcher } from '@fy-tools/rpc-client-nextjs/client';
import { getUsers } from '@/api/users.request';

export function UserList() {
  const { data, isLoading, error } = useFetcher(
    ['users'],         // SWR cache key
    getUsers,          // server action
    { query: { page: '1' } } // payload
  );

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Failed to load users</p>;

  return <ul>{data.items.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Parameters

| Parameter | Type | Description | |---|---|---| | key | Key (SWR) | Cache key — used to identify and deduplicate requests | | apiMethod | ClientApiFunction | The server action to call | | args | InferPayload<TF> | Payload passed to the server action | | options | InferOptions<TF> | Optional Axios request config overrides | | config | SWRConfiguration | Optional SWR config (merges with defaults) |

Default SWR config: shouldRetryOnError: false, revalidateOnFocus: false.


useMutation(key, apiMethod, config?, options?)

SWR mutation hook for POST/PATCH/DELETE operations. Returns a trigger function to call imperatively, plus isMutating, data, and error.

'use client';

import { useMutation } from '@fy-tools/rpc-client-nextjs/client';
import { login } from '@/api/auth.request';

export function LoginForm() {
  const { trigger, isMutating } = useMutation(['login'], login, {
    onSuccess(data) {
      // data is fully typed based on login's response schema
      console.log('Logged in:', data.access);
    },
    errorMessage: 'Login failed. Please try again.',
  });

  return (
    <button
      disabled={isMutating}
      onClick={() => trigger({ body: { username: 'alice', password: 'secret' } })}
    >
      {isMutating ? 'Logging in...' : 'Log in'}
    </button>
  );
}

Parameters

| Parameter | Type | Description | |---|---|---| | key | Key (SWR) | Cache key | | apiMethod | ClientApiFunction | The server action to call | | config | SWRMutationConfiguration & { errorMessage?, successMessage? } | Optional SWR mutation config with extra toast options | | options | InferOptions<TF> | Optional Axios request config overrides |

Automatic toast notifications

useMutation shows error toasts automatically via react-hot-toast:

  • 400 responses: Each message in response.data.message (array) is shown as a separate toast.
  • Other errors: Shows response.data.message, then config.errorMessage, then 'unknown api error'.

Wrap your app with <Toaster /> from react-hot-toast to enable toasts:

import { Toaster } from 'react-hot-toast';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}

Type utilities

| Type | Export | Description | |---|---|---| | ApiResponse<T> | default / server / client | { error: unknown } \| T — the return type of server actions | | ClientApiFunction<TF> | default / server / client | Function signature for a wrapped server action | | ClientFactoryPayload | default / server / client | Options accepted by createApiFactory | | ApiOptions<TF> | default / server / client | Options accepted by createApi |

Use ApiResponse<T> to type server action return values in shared components:

import type { ApiResponse } from '@fy-tools/rpc-client-nextjs';

type TableFetcher<T extends any[]> = () => Promise<
  ApiResponse<{ items: T; total: number }>
>;

Full example

Server actions

// app/_api/posts.request.ts
'use server';

import type { Schema } from '@app/schema';
import { InferError, rpcClient } from '@fy-tools/rpc-client';
import { createApiFactory } from '@fy-tools/rpc-client-nextjs/server';
import { redirect } from 'next/navigation';
import { getAuthToken } from '@/util/auth';

const client = rpcClient<Schema>({ baseURL: process.env.BACKEND_BASE_URL });

const { createApi } = createApiFactory<InferError<Schema>>({
  getAccessToken: async () => (await getAuthToken()).access,
  onAuthorized: () => redirect('/login'),
});

export const listPosts = createApi(client.posts.get_default, {
  tags: ['posts'],
});
export type listPosts = typeof listPosts.$infer;

export const createPost = createApi(client.posts.post_default, {
  isMutation: true,
  tags: ['posts'],
});
export type createPost = typeof createPost.$infer;

export const deletePost = createApi(client.posts.delete_$id, {
  isMutation: true,
  tags: ['posts'],
});
export type deletePost = typeof deletePost.$infer;

Client component

// app/posts/page.tsx
'use client';

import { useFetcher, useMutation } from '@fy-tools/rpc-client-nextjs/client';
import { listPosts, createPost, deletePost } from '@/api/posts.request';

export default function PostsPage() {
  const { data, isLoading, mutate } = useFetcher(['posts'], listPosts, {});

  const { trigger: create, isMutating: isCreating } = useMutation(
    ['posts', 'create'],
    createPost,
    {
      onSuccess: () => mutate(),
      errorMessage: 'Failed to create post.',
    }
  );

  const { trigger: remove } = useMutation(['posts', 'delete'], deletePost, {
    onSuccess: () => mutate(),
  });

  if (isLoading) return <p>Loading...</p>;

  return (
    <div>
      <button
        disabled={isCreating}
        onClick={() => create({ body: { title: 'New post', content: '...' } })}
      >
        New Post
      </button>
      <ul>
        {data?.items.map((post) => (
          <li key={post.id}>
            {post.title}
            <button onClick={() => remove({ params: { id: post.id } })}>
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}