OpenAPI to TypeScript generator and mock server
This project is maintained by pmcelhaney
Counterfact is three complimentary tools in one:
The easiest way to start is to copy and paste this command into your terminal.
npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
This command will generate TypeScript code for the Swagger Pet Store and start the server. We're using the pet store example because it's well known and convenient. If you have your own OpenAPI document handy, you can point to that instead. You can also change api
to wherever you'd like to output the code.
[!NOTE]
Here are the full details on CLI usage
Usage: counterfact [options] [openapi.yaml] [destination] Counterfact is a tool for mocking REST APIs in development. See https://counterfact.dev for more info. Arguments: openapi.yaml path or URL to OpenAPI document or "_" to run without OpenAPI (default: "_") destination path to generated code (default: ".") Options: --port <number> server port number (default: 3100) -o, --open open a browser -g, --generate generate all code for both routes and types --generate-types generate types --generate-routes generate routes -w, --watch generate + watch all code for changes --watch-types generate + watch types for changes --watch-routes generate + watch routes for changes -s, --serve start the mock server -r, --repl start the REPL --proxy-url <string> proxy URL --prefix <string> base path from which routes will be served (e.g. /api/v1) -h, --help display help for command
[!TIP]
Using with npm or yarn
If you prefer not to use
npx
against the@latest
version, you can install Counterfact as a dependency with a specific version in npm or yarn. The following example adds a start script to yourpackage.json
file and adds Counterfact as a dev dependency."scripts": { "start": "npx counterfact https://petstore3.swagger.io/api/v3/openapi.json api" }, "devDependencies": { "counterfact": "^0.38.3", }
This will let your team use the same version of > Counterfact across all environments. You can also use
npm run start
oryarn start
to start the server.
Code is automatically generated and kept in sync with your OpenAPI (aka Swagger) document, assuming you have one. Otherwise, see how to use Counterfact without generating code.
The code goes into two directories:
See Generated Code FAQ for details.
In the routes
directory, you should find a TypeScript file corresponding to each of the paths in your OpenAPI file. For example "/users/{userid}" will create ./routes/users/{userid}.ts
. (If you have a path for the root, "/", it will map to ./routes/index.ts
.) The contents of each file will look something like this:
export const GET: HTTP_GET = ($) => {
return $.response[200].random();
};
export const POST: HTTP_POST = ($) => {
return $.response[200].random();
};
Each of the exported functions implements an HTTP request method (GET, POST, PUT, etc.). Each of these functions takes one argument -- $
-- which is used to access request information, build a response, and interact with the server's state.
[!TIP] If you're familiar with Express,
$
is sort of a combination ofreq
andres
with type safety and extra super powers.
$.response
objectThe $.response
object is used to build a valid response for the URL and request method. This object is designed to work with your IDE's autocomplete feature and help you build a valid response without consulting the docs. Try typing $.response.
in your IDE. You should see a list of numbers corresponding to HTTP response codes (200, 404, etc). Select one and then type another .
. At this point the IDE should present you with one or more of the following methods.
.random()
returns random data, using examples
and other metadata from the OpenAPI document..header(name, value)
adds a response header. It will only show up when a response header is expected and you haven't already provided it..match(contentType, content)
is used to return content which matches the content type. If the API is intended to serve one of multiple content types, depending on the client's Accepts:
header, you can chain multiple match()
calls..json(content)
, .text(content)
, .html(content)
, and .xml(content)
are shorthands for the match()
function, e.g. .text(content)
is shorthand for .match("text/plain", content)
.
.json()
shortcut handles both JSON and XML.To build a response, chain one or more of these functions, e.g.
return $.response[200].header("x-coolness-level", 10).text("This is cool!")`.
[!TIP] Your IDE can help you build a valid response via autocomplete. It can also help ensure the response matches the requirements in the OpenAPI document. For example, if you leave out a required header, the function won't type check. (That's particularly useful when there are API changes. When you update the OpenAPI document, the types are automatically regenerated, and TypeScript tells you if the implementation needs to be updated.)
Most of the time, the server's response depends on input from various parts of the request, which are accessible through $.path
, $.query
, $.header
, and $.body
. The best way to explain is with an example:
export const GET: HTTP_GET = ($) => {
if ($.header['x-token'] !== 'super-secret') {
return $.response[401].text('unauthorized');
}
const content = `TODO: output the results for "${$.query.keyword}"`
+ `in ${$.path.groupName}`
+ `that have the following tags: ${$.body.tags.join(',')}.`.
return $.response[200].text(content);
};
Each of these objects is typed so you can use autocomplete to identify parameters names and types. For example, if you type $.query.
you'll be presented with a list of expected query string parameters.
[!NOTE] The
$.path
parameters are identified by dynamic sections of the file path, i.e./groups/{groupName}/user/{userId}.ts
.
$.context
object and _.context.ts
The $.context
object contains in-memory state and business logic, allowing you to imitate to whatever degree is necessary the behavior of a real API. It looks something like this:
// pet.ts
export const POST: HTTP_POST = ($) => {
return $.response[200].json($.context.addPet($.body));
};
// pet/{id}.ts
export const GET: HTTP_GET ($) => {
const pet = $.context.getPetById($.path.id);
if (pet === undefined) return $.response[404].text(`Pet ${$.path.id} not found.`);
return $.response[200].json(pet);
};
The context
object is an instance of a class exported from ./routes/_.context.ts
. Customize the class to suit your needs. For example, if we're implementing the Swagger Petstore, our _.context.ts
file might look like this.
export class Context {
pets: Pet[] = [];
addPet(pet: Pet) {
const id = this.pets.length;
this.pets.push({ ...pet, id });
return this.getPetById(id);
}
getPetById(id: number) {
return this.pets[id];
}
}
[!IMPORTANT] You can make the context objects do whatever you want, including things like writing to databases. But remember that Counterfact is meant for testing; it's better to "forget" and return to a known state every time you start the server. Keeping everything in memory also makes the server lightning fast.
[!TIP] An object with loadContext() function is passed to the constructor of a context class. You can use it load the context from another directory at runtime. This is an advanced use case.
class Context { constructor({ loadContext }) { this.rootContext = loadContext("/"); } }
$.auth
objectIf a username and password are sent via basic authentication, they can be found via $.auth.username
and $.auth.password
respectively.
Support for other security schemes ("apiKey", "mutualTLS", "oauth2", "openIdConnect") are coming. You can speed things along by opening an issue.
Counterfact does a good job translating an OpenAPI description into TypeScript types. But if your documentation is incorrect or incomplete, or you want to try something that's not documented yet, the type safety can get in your way.
To work around that problem, Counterfact provides a "loose" types mode in the form of the $.x
object. The $.x
object is an alias of $
in which all of the types are wider.
The best way to explain is with a couple of examples.
export function GET($): HTTP_GET {
// There are no headers specified in OpenAPI
$.headers["my-undocumented-header"]; // TypeScript error
$.x.headers["my-undocumented-header"]; // ok
// There is no 500 response type specified in OpenAPI
return $.response[500].text("Error!"); // TypeScript error
return $.x.response[500].text("Error!"); // ok
}
When you save any file changes will be picked up by the running server immediately. There's no need to restart!
Hot reloading supports one of Counterfact's key design goals. While developing and testing, we want to explore counterfactuals, such as
In such cases, we want to be sure the front end code responds appropriately. Getting a real server to do what we need to test front end code is usually difficult if not impossible. Counterfact is optimized to make bending the server's behavior to suit a test case as painless as possible, in both manual and automated tests.
Another way to explore counterfactuals in real time is to interact with the running server via the read-eval-print loop (REPL), in the same way that you interact with running UI code in your browser's developer tools console. If you look in the terminal after starting Counterfact you should see a prompt like this:
____ ____ _ _ _ _ ___ ____ ____ ____ ____ ____ ___
|___ [__] |__| |\| | |=== |--< |--- |--| |___ |
High code, low effort mock REST APIs
| API Base URL ==> http://localhost:3100
| Admin Console ==> http://localhost:3100/counterfact/
| Instructions ==> https://counterfact.dev/docs/usage.html
Starting REPL, type .help for more info
โฌฃ>
At the โฌฃ>
prompt, you can enter JavaScript code to interact with the live context object. For example, here's a quick way to add a pet to the store.
context.addPet({ name: "Fluffy", photoUrls: [] });
Or add 100 pets:
for (i = 0; i < 100; i++) context.addPet({ name: `Pet ${i}`, photoUrls: [] });
Or get a list of pets whose names start with "F"
context.pets.find((pet) => pet.name.startsWith("F"));
Using the REPL is a lot faster (and more fun) than wrangling config files and SQL and whatever else it takes to get a real back end into the states you need to test your UI flows.
[!TIP]
For large / complex APIs, a single context object may not be sufficient. Any subdirectory can provide its own context object by including a
_.context.ts
file that exports a class calledContext
. In the REPL, to access context object outside of the root, useloadContext("/path/to/subdirectory")
.โฌฃ> const petsContext = loadContext("/pets");
The
loadContext()
function is also passed to the constructor ofContext
so that one context object can access another.// ./routes/users/_.context.ts export class Context() { constructor({ loadContext }) { this.rootContext = loadContext("/"); this.petsContext = loadContext("/pets"); }
At some point you're going to want to test your code against a real server. At that point, you could throw the mock server away. However, you may wish you's kept it around testing edge cases and back-end changes that are still in development.
Why not both? ๐คทโโ๏ธ
Counterfact has a couple of facilities proxy to the real server for the most part, but continue using mocks on a case-by-case basis.
To proxy an individual endpoint, you can use the $.proxy()
function.
// pet/{id}.ts
export const GET: HTTP_GET ($) => {
return $.proxy("http://uat.petstore.example.com/pet")
};
To set up a proxy for the entire API, add --proxy <url>
in the CLI and / or type .proxy url <url>
in the CLI.
From there, you can switch back and forth between the proxy and mocks by typing .proxy [on|off] <path-prefix>
. Type .proxy help
for detailed information on using the .proxy
command.
Even if you're not using mocks at all, the proxy feature is convenient for switching between different back end environments -- local, dev, QA, etc -- without changing configuration files or restarting.
With convention over configuration, automatically generated types, a fluent API, and an innovative REPL, Counterfact allows front-end developers to quickly build fake REST APIs for prototype and testing purposes.
_.context.ts
files. These are created for you, but you should modify them to suit your needs.More features are coming soon:
Please send feedback / questions to pmcelhaney@gmail.com or create a new issue.
And yes, contributions are welcome!