Skip to main content

Frameworks

The test client sends requests into your app in the same process as your tests. Requests go through the same middleware, error handlers and framework glue as in production, without deploying anything.

Pass your app as the app option, and the client works out what kind of app it is:

FrameworkPass as app
Expressyour Express app
Fastifyyour Fastify instance
Koayour Koa app
Hapiyour Hapi server
Node.jsan http.Server, or a (req, res) request listener
Hono, Elysia and other fetch appsan object with a fetch(request) method
AWS Lambdalambda(handler)
A running serverits URL
Anything elsean object with a send(request) method
No frameworkleave out app, and pass transformResponse
tip

Export your app from a module that doesn't call app.listen(), and start the server in a separate entry point. Then your tests can import the app without starting it.

Express​

src/app.ts
export const app = express();
app.use(express.json());
app.use((req, res, next) => api.handleRequest(req, req, res).catch(next));
test/client.ts
export const client = createTestClient(api, { app });

The first time it sends a request, the client serves your app on a random port on 127.0.0.1, like supertest does. The server doesn't keep your test process running.

The same works for any framework that gives you a Node.js request listener, like Connect or Polka.

Fastify​

src/app.ts
export const app = Fastify();
app.route({
method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
url: '/*',
handler: (request, reply) =>
api.handleRequest(
{
method: request.method,
path: request.url,
body: request.body,
query: request.query,
headers: request.headers,
},
request,
reply,
),
});
test/client.ts
export const client = createTestClient(api, { app });

The client uses Fastify's inject(), so you don't need to call app.listen() or app.ready().

Koa​

src/app.ts
export const app = new Koa();
app.use(bodyparser());
app.use((ctx) => api.handleRequest(ctx.request, ctx));
test/client.ts
export const client = createTestClient(api, { app });

The client serves app.callback() on a random local port, like it does for Express.

Hapi​

src/server.ts
export const server = Hapi.server();
server.route({
method: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
path: '/{path*}',
handler: (req, h) =>
api.handleRequest(
{
method: req.method,
path: req.path,
body: req.payload,
query: req.query,
headers: req.headers,
},
req,
h,
),
});
test/client.ts
import { api, server } from '../src/server';

await server.initialize();

export const client = createTestClient(api, { app: server });

The client uses Hapi's server.inject(). Call server.initialize() first, but not server.start().

Node.js http​

Pass an http.Server, or the request listener you'd pass to http.createServer():

test/client.ts
import { api, server } from '../src/server';

export const client = createTestClient(api, { app: server });

The client serves the server's request listener on a random local port. It doesn't call listen() on your server.

Fetch apps​

Frameworks built on the web-standard Request and Response, like Hono or Elysia, have a fetch() method. Pass the app:

src/app.ts
export const app = new Hono();
app.all('*', async (c) =>
api.handleRequest(
{
method: c.req.method,
path: c.req.path,
query: c.req.query(),
headers: c.req.header(),
body: await c.req.text(),
},
c,
),
);
test/client.ts
export const client = createTestClient(api, { app });

For a plain (request) => Response function, like a Next.js route handler, pass { fetch: handler }.

AWS Lambda​

Wrap your Lambda handler with lambda():

src/lambda.ts
export const handler = (event, context) =>
api.handleRequest(
{
method: event.httpMethod,
path: event.path,
query: event.queryStringParameters,
body: event.body,
headers: event.headers,
},
event,
context,
);
test/client.ts
import { createTestClient, lambda } from 'openapi-backend/testing';
import { api, handler } from '../src/lambda';

export const client = createTestClient(api, { app: lambda(handler) });

The client turns each request into an API Gateway event, calls your handler, and reads the response from its result, like API Gateway does. Results without a content-type header are sent as application/json.

By default, events look like they come from an API Gateway REST API. For HTTP APIs and function URLs, use lambda(handler, { event: 'v2' }). See the reference for all options.

A running server​

Pass a URL to send requests over the network, for example for smoke tests against a staging environment:

test/smoke/client.ts
export const client = createTestClient(api, {
app: 'https://staging.example.com',
});

Requests are built from your spec and responses are checked against it, as usual. But the client can't see inside another process, so res.context is undefined, and security stubs throw an error.

Custom adapters​

For anything else, pass an object with a send() method. It gets the request the client built, which is also a valid openapi-backend Request object, and returns the response as { status, headers, body } or as a fetch Response:

const client = createTestClient(api, {
app: {
async send(request) {
const result = await myPlatform.invoke(toPlatformRequest(request));
return { status: result.code, headers: result.headers, body: result.payload };
},
},
});

String and Buffer bodies are parsed according to the content-type header. Other values are used as the body as they are.

No framework​

If your handlers don't depend on a framework, you don't need an app. Leave out app, and the client calls api.handleRequest() directly. openapi-backend doesn't know what your handlers return, so tell the client how to turn it into a response with transformResponse:

const api = new OpenAPIBackend({
definition: './openapi.yml',
handlers: {
getPets: async () => ({ statusCode: 200, body: await db.listPets() }),
validationFail: (c) => ({
statusCode: 400,
body: { message: 'Bad request', errors: c.validation.errors },
}),
},
});

const client = createTestClient(api, {
transformResponse: (result) => ({
status: result.statusCode,
headers: { 'content-type': 'application/json', ...result.headers },
body: result.body,
}),
});

transformResponse gets the value api.handleRequest() returned, and returns { status, headers, body } or a fetch Response, like a custom adapter.

Request bodies reach api.handleRequest() the way a body parser would pass them on: JSON as parsed objects, form fields as strings, and other bodies as strings or Buffers.

If api.handleRequest() throws, for example with a 400-validationFail error in strict mode, the call throws the same error.

Base path​

The client prepends your API's apiRoot to the paths of operations. If your app serves the API under another path, set basePath:

// the app serves openapi-backend under /api
const client = createTestClient(api, { app, basePath: '/api' });

await client.getPets(); // GET /api/pets

How the client sees what happened​

To link each request to the context openapi-backend builds for it, the client adds an x-openapi-backend-test-id header to requests it handles in the same process. Your app needs to pass the request headers to api.handleRequest(), which it does anyway, so they can be validated. The header isn't included in res.request.