Summary
OpenAPI, formerly swagger is a specification for RESTful interfaces that helps developers and engineers write and consume an API.
Typescript is a strongly typed programming language built on Javascript.
Most Typescript projects that utilise HTTP interfaces rely on an HTTP interface. There are many ways to create an OpenAPI specification for that interface, that is strongly typed to your Typescript.
I have conducted a review of the existing tooling, highlighting three ways that typescript and the OpenAPI specification can interact.
Conclusion
If I had to pick, I would write my OpenAPI specification as a typed object, using typescript. Then I would want typechecking for my request/responses, but I would not want prescriptive code, and I would not use generation in the lifecycle, because it shouldn't be necessary.
Compeller
So, because of that I am writing compeller, A strong typescript binding for your OpenAPI Schema that doesn't need generation and is not prescriptive in coding style.
simonireilly / compeller
A strong typescript binding for your OpenAPI Schema that doesn't need generation and is not prescriptive in coding style
Compeller
A strong typescript binding for your OpenAPI Schema that doesn't need generation, and isn't prescriptive.
🚨 Alpha software 🚨
Compeller is in alpha, so it's API might change, maybe you have some thoughts?
About
Compeller tries to infer your OpenAPI validations and responses, from a typed OpenAPI specification.
Get started
You can use the CLI to start a new project, generating an OpenAPI specification.
npx compeller@alpha new
🛣️ Road map
- Support for request body validation to type guard (ajv)
- Support for header response types
- Support for response type mapping
- Support for path validation
- Support header validation
Usage
Create a Schema specification for an API Model like:
// ./examples/standalone/openapi/schemas/version.schema.ts
import { FromSchema } from 'json-schema-to-ts';
export const VersionSchema = {
type: 'object',
required: ['version'],
additionalProperties: false,
properties:
…You can check it out with:
npx compeller@alpha new
Typescript - OpenAPI Mapping
At a high level we might say that we can either be OpenAPI specification driven, or Typescript code driven. These two approaches provide the following options.
1️⃣ Define an OpenAPI specification, and from that, generate the Typescript types for the API domain.
2️⃣ Use decorators on Typescript to produce an OpenAPI specification from the API domain.
There is a wildcard, type 3, where you an bind the typescript to the OpenAPI document by placing all or parts of the OpenAPI specification inside typescript itself. This approach does not use generation, but instead uses type inference to ensure the types and their schema are in agreement.
3️⃣ Combine the OpenAPI specification, and API domain types, into a single Typescript object.
1️⃣ OpenAPI => Typescript
The team behind the OpenAPI specification provided an example here.
We have our specification file from the above link. So lets review some of the tools we can ue to get our types from the specification:
OpenAPI Generator
Link: https://github.com/OpenAPITools/openapi-generator
Generate clients, and stub servers in multiple languages from the OpenAPI specification.
- Generate client
- Generate server - No TS support
- Generate types
Usage
The below example creates a typescript API Client.
docker run \
--rm \
-v "${PWD}:/local" \
openapitools/openapi-generator-cli generate \
-i /local/spec.json \
-g typescript-axios \
-o /local/openapi-generator/ts
The created client will dutifully represent your types; here is the NewPet
schema as an interface, automatically generated by the tool:
/**
*
* @export
* @interface NewPet
*/
export interface NewPet {
/**
*
* @type {string}
* @memberof NewPet
*/
'name': string;
/**
*
* @type {string}
* @memberof NewPet
*/
'tag'?: string;
}
There is not yet support for generating a typescript server.
OpenAPI Typescript Codegen
Link: https://github.com/ferdikoomen/openapi-typescript-codegen
Produce typescript api clients from OpenAPI specification. Build tooling is not Java
Supports:
- Generate typescript clients
Usage
You can generate a client from the specification with:
npx openapi-typescript-codegen \
--input ./spec.json \
--output ./openapi-typescript-codegen \
--exportSchemas true
The provided client can be used to fetch from the API:
import { PetsService } from './index';
const pets = await PetsService.listPets(5);
In this instance pets will be typed either as Pet | Error
after the fetch promise resolves.
Openapi Typescript
Link: https://github.com/drwpow/openapi-typescript
Produce typescript interfaces, from OpenAPI schema
Supports:
- Generate types
Usage
The below example creates Typescript types and interfaces that represent the API Document.
npx openapi-typescript spec.json \
--output ./openapi-typescript/schema.ts
The created types include paths, operations, and components. Here is an example of the paths:
export interface paths {
'/pets': {
get: operations['listPets'];
post: operations['createPets'];
};
'/pets/{petId}': {
get: operations['showPetById'];
};
}
The operations section is automatically generated. I can imagine this would be very useful.
You can build a simple response handler, that is not coupled to any particular framework like so:
import { operations } from './schema';
type showPetById = operations['showPetById'];
const response = <K extends keyof showPetById['responses']>(
statusCode: K,
body: showPetById['responses'][K]['content']['application/json']
) => {};
This binds the response codes to the response objects, which are inturn bound to your OpenAPI schema via the generation.
2️⃣ Typescript => OpenAPI
In this scenario we have our Typescript code, for our server, and we want to create an OpenAPI specification.
These decorator and binding methods require you to write your code in a specific way, which is restrictive, and not adoptable for existing large projects.
NestJS
link: https://docs.nestjs.com/openapi/introduction
NestJS is a progressive typescript framework for building backend applications.
Supports:
- Rendering documentation
- Adding docs to controllers using decorators
- Express and Fastify support
The documentation for this tool is extensive, so I will leave it to their talented team to explain the usage.
TSOA
link: https://tsoa-community.github.io/docs/
TSOA generates routes from your Typescript code for koa, fastify or express
- Rendering documentation
- Adding docs to controllers using decorators
- Express and Fastify support
This tool has code bindings and decorators, but also makes use of generation, so there is a lot of configuration overhead.
3️⃣ Typescript <=> OpenAPI
This collection of tools bring the OpenAPI specification domain into the typescript typing system.
This gives some benefits when developers are more used to receiving type hints. Whilst it prevents the schema from being a portable JSON document, you can always export the javascript object to a JSON or YAML file.
This method is in my opinion less opaque, as it does not use generation like method 1️⃣, and less prescriptive than method 2️⃣, because you can write your code however you should like.
Basically I am all in on method 3️⃣!
OpenAPI3 TS
Usage
The extensive type system of OpenAPI3-TS attempts to be true to the OpenAPI specification.
const spec: OpenAPIObject = {
info: {
title: 'Mandatory',
version: '1.0.0',
},
openapi: '3.0.3',
paths: {
'/v1/version': {
get: {
responses: {
'200': {
description: 'example',
content: {
'application/json': {
schema: {
type: 'string',
},
},
},
} as ResponseObject,
},
},
} as PathItemObject,
},
};
This has advantages over other type systems, because it will require a diligent documentation of the types to ensure the compiler agrees.
AJV JSONSchemaType
Link: https://ajv.js.org/guide/typescript.html#utility-types-for-schemas
This utility type can take the API models that you declare as type aliases, or interfaces, and infer a fully JSON schema for the type.
Supports
- Inferring schema type from object type.
Usage
This example is borrowed from te documentation.
import Ajv, {JSONSchemaType} from "ajv"
const ajv = new Ajv()
interface MyData {
foo: number
bar?: string
}
const schema: JSONSchemaType<MyData> = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string", nullable: true}
},
required: ["foo"],
additionalProperties: false
}
// validate is a type guard for MyData - type is inferred from schema type
const validate = ajv.compile(schema)
// or, if you did not use type annotation for the schema,
// type parameter can be used to make it type guard:
// const validate = ajv.compile<MyData>(schema)
const data = {
foo: 1,
bar: "abc"
}
if (validate(data)) {
// data is MyData here
console.log(data.foo)
} else {
console.log(validate.errors)
}
The benefit of this method, is there is a runtime binding between the schema validation, and the typing system.
JSON Schema to TS
Link: https://github.com/ThomasAribart/json-schema-to-ts
Given a schema object in typescript, the type it represents can be inferred.
Supports:
- Inferring Schema to Object type
Usage
The FromSchema
type requires the schema to be made a const, and this means the schema itself is missing type hints.
import { FromSchema } from 'json-schema-to-ts';
export const VersionSchema = {
type: 'object',
required: ['version'],
additionalProperties: false,
properties: {
version: {
type: 'string',
},
},
} as const;
export type Version = FromSchema<typeof VersionSchema>;
This is a little less convenient as you still need to know the OpenAPI specification, and the type will resolve to never
with no error if you get it wrong.
Closing
Thanks for reading this review, if there are any issues with the links let me know 👍
Top comments (1)
can bind