Testing and deploying to production
Twenty-one parts in, devnotes has real routing, data fetching, SEO, a comment form, and a sitemap. This last part covers what's worth testing before it ships, and getting it live.
Testing a Server Action directly
npm install -D vitest// app/actions.test.ts
import { describe, it, expect } from "vitest";
import { submitComment } from "./actions";
describe("submitComment", () => {
it("rejects a comment under 3 characters", async () => {
const formData = new FormData();
formData.set("body", "hi");
const result = await submitComment("hello-app-router", {}, formData);
expect(result.error).toBe("Comment needs at least 3 characters.");
});
it("accepts a valid comment", async () => {
const formData = new FormData();
formData.set("body", "This was a genuinely helpful post.");
const result = await submitComment("hello-app-router", {}, formData);
expect(result.success).toBe(true);
});
});Part 15's submitComment is a plain async function underneath the "use server" directive — calling it directly in a test, passing a real FormData object, needs no special Next.js test harness, no rendered component, no browser. This is the same principle the Django and DRF series applied to their own validation logic: the highest-value tests are the ones covering real logic (the length check here) rather than trivial rendering.
What's worth testing beyond that
- Server Actions with validation — exactly like above; this is where real logic tends to live in an App Router project
- Utility functions — anything in
lib/, likegetAllPosts()'s filtering logic once it's more than a hardcoded array - Critical rendered output — a component test (React Testing Library, alongside Vitest) for something like
PostCardrendering the right title and link, if the project's component logic grows complex enough to warrant it
Full page-level and routing behavior is generally better covered by end-to-end tools (Playwright, the same one used to verify pages throughout this very site's own build process) than by unit tests reaching into Next.js's routing internals directly.
Deploying to Vercel
npm install -g vercel
vercelVercel is built by the same team as Next.js, and it's the deployment target every rendering strategy from part 17 works on with zero extra configuration — SSG pages served from its CDN, SSR and Server Actions running as serverless functions automatically, ISR's background regeneration handled natively. Running vercel from the project root deploys it directly; connecting the same project to a GitHub repository through Vercel's dashboard adds automatic deployments on every push instead.
Setting environment variables in production
Part 16's .env.local never gets deployed — it's gitignored, and Vercel has no access to a file that was never committed. Every variable from it needs to be set again, explicitly, in the hosting platform's own dashboard (Vercel's Project Settings → Environment Variables, or the equivalent on any other host) — DATABASE_URL and any other real secret, entered there rather than committed anywhere.
After deploying, check /sitemap.xml and /robots.txt first — both resolving correctly with real post URLs confirms the deployed build has the right data and environment before debugging anything more specific, the same health-check-first instinct the REST API series' own deployment parts both closed on.
What you built
Twenty-two parts back, this was npx create-next-app. It's now a real blog with static and dynamic routes, Server and Client Components used deliberately rather than by default, full SEO metadata and JSON-LD, an optimized image and font pipeline, a working comment form built on Server Actions, middleware, and a sitemap that stays accurate on its own. That shape — server-first by default, client-side JavaScript only where interactivity actually demands it — is the foundation nearly every real Next.js App Router project is built from.