Next.js
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
.
export type Result<T = void> = {
success: boolean;
message?: string;
data?: T;
};
"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.
<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.
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
andgetRewrittenUrl
See Full Code
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"]