Testing Actions and Route Handlers

Route Handlers Are Functions

Import the handler, pass a Request, and check the Response. Mock the data layer so no database is needed.

// app/api/products/[id]/route.test.ts
import { describe, expect, it, vi } from 'vitest';

vi.mock('@/lib/data/products', () => ({
  getProduct: vi.fn(async (id: number) => (id === 1 ? { id: 1, name: 'Clean Code', price: 450 } : null)),
}));

import { GET } from './route';

describe('GET /api/products/[id]', () => {
  it('returns 404 for a missing product', async () => {
    const res = await GET(new Request('http://test/api/products/9'), { params: Promise.resolve({ id: '9' }) });
    expect(res.status).toBe(404);
  });
});

Server Actions

A Server Action is an async function; call it with a FormData. Mock auth(), revalidatePath and redirect:

vi.mock('@/auth', () => ({ auth: vi.fn(async () => ({ user: { id: 'u1', role: 'admin' } })) }));
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));

import { createProduct } from './actions';

it('returns field errors for bad input', async () => {
  const fd = new FormData();
  fd.set('name', 'TV');
  fd.set('price', '-1');
  const state = await createProduct({}, fd);
  expect(state.errors?.price?.[0]).toBe('Price must be more than 0');
});