Testing

Testing is a first-class feature of Server.js. Call .test() on your app to get a lightweight test client that runs requests through the full middleware stack without starting an HTTP server.

Basic setup

Keep your server in its own file so tests can import it:

// src/index.js
import server from '@server/next';

export default server()
  .get('/', () => 'Hello world')
  .get('/users', () => User.list())
  .post('/users', (ctx) => User.create(ctx.body));
// src/index.test.js
import app from './index.js';

const api = app.test();

it('returns hello world', async () => {
  const res = await api.get('/');
  expect(await res.text()).toBe('Hello world');
});

Available methods

The test client mirrors the HTTP methods. Each call returns a standard Response:

const api = app.test();

api.get('/path', options?)
api.post('/path', body?, options?)
api.put('/path', body?, options?)
api.patch('/path', body?, options?)
api.delete('/path', options?)
api.head('/path', options?)
api.options('/path', options?)
  • body: the request body. Accepts a string, plain object (serialized as JSON), FormData, or ReadableStream.
  • options: standard RequestInit options (e.g. headers).

Reading the response

Each method returns a standard web Response:

const res = await api.get('/users');

res.status              // 200
res.headers.get('content-type')  // 'application/json'
await res.text()        // raw body as string
await res.json()        // parsed JSON body

Sending a body

Pass a plain object and Server.js will serialize it as JSON and set Content-Type: application/json automatically:

const res = await api.post('/users', {
  name: 'Francisco',
  email: '[email protected]',
});
expect(res.status).toBe(201);
expect(await res.json()).toMatchObject({
  name: 'Francisco',
});

For other body types:

// Plain text
await api.post('/echo', 'Hello world');

// FormData (e.g. file uploads)
const form = new FormData();
form.append('name', 'Francisco');
await api.post('/upload', form);

// Custom headers
await api.get('/secure', {
  headers: { authorization: 'Bearer token123' },
});

Full example

// src/books.test.js
import app from './index.js';

const api = app.test();

describe('books API', () => {
  it('lists books', async () => {
    const res = await api.get('/books');
    expect(res.status).toBe(200);
    expect(await res.json()).toEqual([]);
  });

  it('creates a book', async () => {
    const res = await api.post('/books', {
      title: 'Dune',
      author: 'Herbert',
    });
    expect(res.status).toBe(201);
    const body = await res.json();
    expect(body.title).toBe('Dune');
  });

  it('returns 404 for unknown books', async () => {
    const res = await api.get('/books/9999');
    expect(res.status).toBe(404);
  });

  it('deletes a book', async () => {
    const res = await api.delete('/books/1');
    expect(res.status).toBe(204);
  });
});

Limitations

The test client simulates requests in-process; it does not open a real TCP connection. This means:

  • Compression (brotli, gzip) and content-encoding headers are not applied (those are usually set by edge proxies).
  • Edge-only features like cf-* headers won't be present.

For integration tests that require a real HTTP connection, start the server on a test port and use fetch directly.