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
.
import type { typeToFlattenedError } from "zod";
type SuccessResult<T> = {
success: true;
data: T;
message?: string;
};
type FailureResult<T, U> = {
success: false;
message?: string;
data?: U;
zodErrors?: typeToFlattenedError<T>["fieldErrors"];
};
/**
* @typeParam T - data to be returned if successful
* @typeParam U - validation error by zod
* @typeParam P - data to be returned if failed
*/
export type Result<T = void, U = Record<string, unknown>, P = never> =
| SuccessResult<T>
| FailureResult<U, P>;
"use server";
import type { Session } from "next-auth";
import { auth } from "../_clients/nextAuth";
import type { Result } from "./types";
export async function getSessionOrReject(): Promise<Result<Session, void>> {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
message: "no session token",
};
}
return {
success: true,
data: session,
};
} catch {
return {
success: false,
message: "no session token",
};
}
}
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 { beforeEach } from "node:test";
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", () => {
beforeEach(() => {
vi.mock("next-auth", async (actual) => ({
...(await actual<typeof import("next-auth")>()),
default: () => ({
auth: (
fn: (
req: NextAuthRequest,
ctx: AppRouteHandlerFn,
) => Promise<NextResponse>,
) => fn,
}),
}));
});
test("should execute middleware when paths are specified by config", () => {
const fixtures: [string, boolean][] = [
["/", false],
["/me", true],
];
for (const [url, expected] of fixtures) {
expect(
unstable_doesMiddlewareMatch({
config,
nextConfig,
url,
}),
).toEqual(expected);
}
});
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 = {
auth: {
user: {
role: "USER",
},
expires: "expires",
},
} as NextAuthRequest;
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.14.0-slim AS base
ARG DATABASE_USER=''
ARG DATABASE_PASSWORD=''
ARG DATABASE_DB=''
ARG DATABASE_HOST=''
ARG DATABASE_PORT=''
ARG DATABASE_SCHEMA=''
ARG GOOGLE_CLIENT_ID=''
ARG GOOGLE_CLIENT_SECRET=''
ARG NEXT_PUBLIC_SITE_URL=''
ARG NEXTAUTH_SECRET=''
# start: otel #
ARG TRACE_EXPORTER_URL=''
# end: otel #
# start: stripe #
ARG STRIPE_PRICE_ID=''
ARG STRIPE_SECRET_KEY=''
ARG STRIPE_WEBHOOK_SECRET=''
# end: stripe #
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV DATABASE_USER=$DATABASE_USER
ENV DATABASE_PASSWORD=$DATABASE_PASSWORD
ENV DATABASE_DB=$DATABASE_DB
ENV DATABASE_HOST=$DATABASE_HOST
ENV DATABASE_PORT=$DATABASE_PORT
ENV DATABASE_SCHEMA=$DATABASE_SCHEMA
ENV GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID
ENV GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
ENV NEXTAUTH_URL=$NEXT_PUBLIC_SITE_URL
# start: otel #
ENV TRACE_EXPORTER_URL=$TRACE_EXPORTER_URL
# end: otel #
# start: stripe #
ENV STRIPE_PRICE_ID=$STRIPE_PRICE_ID
ENV STRIPE_SECRET_KEY=$STRIPE_SECRET_KEY
ENV STRIPE_WEBHOOK_SECRET=$STRIPE_WEBHOOK_SECRET
# end: stripe #
COPY . /app
WORKDIR /app
RUN npm run setup
# for prisma
RUN apt-get update -y && apt-get install -y openssl
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm i --prod --frozen-lockfile
RUN pnpm prisma generate --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=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/.next /app/.next
EXPOSE 3000
CMD ["pnpm", "start"]