Skip to content

Next.js

nextjsreact-hook-formzoddocker

Server Actions

Result Type Best Practice

If you throw an Error in a server action, it will be caught by ErrorBoundary and error.tsx will be called. However, this is only an unexpected error; in most cases of the real world, errors will be expected. In that case, returning an object such as a message is common instead of throwing an error. Also, the Result type is friendly for useActionState.

ts
export type Result<T = void> = {
  success: boolean;
  message?: string;
  data?: T;
};
ts
"use server";

import { revalidatePath } from "next/cache";
import { auth } from "../_clients/nextAuth";
import { prisma } from "../_clients/prisma";
import { type UserMeSchema, userMeSchema } from "../_schemas/users";
import type { Result } from "./types";

type UpdateMeState = Result<PartialWithNullable<UserMeSchema>>;

export async function updateMe(
  prevState: UpdateMeState,
  formData: FormData,
): Promise<UpdateMeState> {
  const session = await auth();

  if (!session) {
    return {
      success: false,
      message: "no session token",
    };
  }

  const data: PartialWithNullable<UserMeSchema> = {
    name: session.user.name,
    email: session.user.email,
    image: session.user.image,
    ...Object.fromEntries(formData.entries()),
  };

  const validatedFields = userMeSchema.safeParse(data);

  if (!validatedFields.success) {
    return {
      success: false,
      message: "invalid fields",
    };
  }

  await prisma.$transaction(async (prisma) => {
    return await prisma.user.update({
      where: {
        id: session.user.id,
      },
      data,
    });
  });

  revalidatePath("/");
  revalidatePath("/me");

  return {
    success: true,
    message: "updated",
    data,
  };
}

WARNING

The action of Form requires Promise<void> so when using Form directly, need to avoid using the Result type.

ts
<form action={submitAction} />

Middleware

Authentication and Authorization Best Practice

It is common to use middleware to check a user's role, etc., and restrict access to pages. This template uses JSON Web Token as session tokens to check the permissions. Learn more here.

ts
import NextAuth from "next-auth";
import { NextResponse } from "next/server";
import { config as authConfig } from "./app/_clients/nextAuthConfig";

const { auth } = NextAuth(authConfig);

export const config = { matcher: ["/me"] };

export default auth(async function middleware(req) {
  if (req.auth?.user.role === "user") {
    return NextResponse.next();
  }

  return NextResponse.rewrite(new URL("/signin", req.url));
});

Testing

WARNING

This feature is experimental

Next.js provides utilities to help unit test middleware files.

conducting tests based on the following aspects:

  • Is the routing working correctly? (without executing the middleware logic)
    • unstable_doesMiddlewareMatch
  • Is the middleware logic functioning as expected?
    • isRewrite and getRewrittenUrl
See Full Code
ts
import type { NextAuthResult } from "next-auth";
import type { AppRouteHandlerFn } from "next/dist/server/route-modules/app-route/module";
import {
  getRewrittenUrl,
  isRewrite,
  unstable_doesMiddlewareMatch,
} from "next/experimental/testing/server.js";
import { NextRequest, type NextResponse } from "next/server";
import { describe, expect, test, vi } from "vitest";
import nextConfig from "../next.config";
import middleware, { config } from "./middleware";

type NextAuthRequest = Parameters<Parameters<NextAuthResult["auth"]>[0]>[0];

describe("middleware", () => {
  // need to mock next-auth to avoid errors
  // Error: Cannot find module '/node_modules/.pnpm/next-auth@5.0.0-beta.25_next@15.1.3_@babel+core@7.26.0_@opentelemetry+api@1.8.0_@playwright+t_d57mf5jazi4hxealio4ynmcldm/node_modules/next/server'
  // imported from /node_modules/.pnpm/next-auth@5.0.0-beta.25_next@15.1.3_@babel+core@7.26.0_@opentelemetry+api@1.8.0_@playwright+t_d57mf5jazi4hxealio4ynmcldm/node_modules/next-auth/lib/env.js
  // Did you mean to import "next/server.js"?
  vi.mock("next-auth", () => ({
    default: () => ({
      auth: (
        fn: (
          req: NextAuthRequest,
          ctx: AppRouteHandlerFn,
        ) => Promise<NextResponse>,
      ) => fn,
    }),
  }));

  test("should execute middleware when paths are specified by config", () => {
    expect(
      unstable_doesMiddlewareMatch({
        config,
        nextConfig,
        url: "/",
      }),
    ).toEqual(false);

    expect(
      unstable_doesMiddlewareMatch({
        config,
        nextConfig,
        url: "/me",
      }),
    ).toEqual(true);
  });

  test("should route /signin to when fallback", async () => {
    const req = new NextRequest("http://localhost:3000");
    const res = (await middleware(req, {})) as NextResponse;

    expect(isRewrite(res)).toEqual(true);
    expect(getRewrittenUrl(res)).toEqual("http://localhost:3000/signin");
  });

  test("should accept only users having role of user", async () => {
    const req = new NextRequest("http://localhost:3000") as NextAuthRequest;

    req.auth = {
      user: {
        role: "user",
      },
      expires: "expires",
    };

    const res = (await middleware(req, {})) as NextResponse;

    expect(isRewrite(res)).toEqual(false);
    expect(getRewrittenUrl(res)).toEqual(null);
  });
});

Building with Docker Optional

This template provides a Dockerfile for your application.

See Full Code
FROM node:22.12.0-slim AS base

WORKDIR /app

ARG DATABASE_URL=''
ARG NEXTAUTH_SECRET=''
ARG NEXT_PUBLIC_SITE_URL=''
ARG TRACE_EXPORTER_URL=''

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV DATABASE_URL=$DATABASE_URL
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
ENV NEXTAUTH_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV TRACE_EXPORTER_URL=$TRACE_EXPORTER_URL

RUN corepack enable
RUN apt-get update -y && apt-get install -y openssl

COPY . /app

FROM base AS prod-deps

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i --prod --frozen-lockfile
RUN pnpm generate:client --generator client

FROM base AS build

RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i --frozen-lockfile
RUN pnpm build

FROM base AS app

COPY --from=build /app/.next /app/.next
COPY --from=prod-deps /app/node_modules /app/node_modules

EXPOSE 3000
CMD ["pnpm", "start"]