- merge all APIPromise class - remove IServerAPI and IClientAPI - remove res Object, return [status, body] in ServerAPI instead - remove schema classes, use interface instead - `-s` flag for `ctx.state` interface path
15 KiB
OpenAPI codegen for TypeScript
What is this?
This is a TypeScript code generator which generates TypeScript types and interfaces base on your OpenAPI document, including
schemas
IHandler
, types and interfaces for both server and client apiapiRouter
, server api partial implementation using koa routerClientAPI
, client api implementation using axios
This tool assumes you use koa router for server and axios for client.
How to use this tool?
0. Install it
yarn add -D @supmiku39/api-ts-gen
Also, install the dependencies that generated code will use.
yarn add koa @koa/router koa-body
yarn add -D @types/koa @types/koa__router axios
1. Write your OpenAPI document
You can use Swagger Editor to edit your api document. Make sure to follow the OpenAPI Specification.
This tool generates code from paths
and components/schemas
.
⚠️ only get
, post
, update
, delete
, patch
are supported in paths
2. Set path alias
Determine the output directory for generated code and the server api implementation location, and edit your tsconfig.json
:
{
// ...
"compilerOptions": {
// ...
"baseUrl": ".", // or your project root
"paths": {
// ...
"#api/*": ["path/to/your/output/directory/for/generated/code/*"],
"#ServerAPI": ["path/to/your/server/api/implementation"]
}
}
}
If you use webpack, you should also edit your webpack.config.js
:
module.exports = {
// ...
resolve: {
// ...
alias: {
'#api': 'path/to/your/output/directory/for/generated/code',
// #ServerAPI is not required
},
},
};
3. Run this tool
yarn run api-codegen <your-openapi-document> [-o <output-dir>] [-s <ctx.state-interface-path>]
The default output directory is api/generated
.
For example, if you put your api document at api.yml
, and want the generated code put in generated/
directory, you can execute
# example
yarn run api-codegen api.yml -o generated
4. Implement server api
import {IServerAPI} from '#api/IServerAPI';
export default {
operationId: async (req, state, ctx) => {
// ...
},
// ...
} as IServerAPI;
The function name is the operationId
defined in the api document.
There are 3 arguments passed to the function.
req
The request parameters and body, defined in parameters
and requestBody
.
Any parameter will be put in req.{in}.{name}
, where {in}
is one of path
, query
, header
, cookie
.
requestBody
will be put in req.body
.
state
Alias to ctx.state
ctx
The ctx
object from koa router. Avoid to use it unless required.
// Don't do this unless required
ctx.body = responseBody;
ctx.status = statusCode;
// Do this
res[statusCode](responseBody);
return value
[status, body]
If the responseBody is not required, you can omit it or pass anything to it.
// example
return [200, {...}];
return [404, 'some response string'];
// if responseBody is not required
return [statusCode]; // OK
return [statusCode, 'message string']; // OK
return [statusCode, {some: 'object'}]; // OK
5. Mount the api router to server
import * as Koa from 'koa';
import apiRouter from '#api/apiRouter';
const app = new Koa();
// some other entry
app.use(apiRouter.prefix('/your/api/prefix').routes());
// or simply app.use(apiRouter.routes());
app.listen(yourAppListenPort);
6. Use api in client
Simply import and use it! Like the server api, the function name will be the operationId
defined in the api document, and the parameters will be the parameters
and requestBody
if defined.
api.{operationId}([path], [query], [header], [cookie], [body])
where the path
, query
, header
, cookie
is a object whose key is the name
defined in parameters
in the api document, and body
is the requestBody
.
💡 Note that if the method is patch
, req.body
will be Partial
, which means that any property can be omitted.
import api from '#api/ClientAPI';
// import {FullDate} from '@supmiku39/api-ts-gen/utils';
// import {SchemaA} from '#api/schemas';
// ...
api.operationWithPath({pathName: pathValue});
api.operationWithQueryAndBody({queryName: queryValue}, body);
api.operationWithPathAndQueryAndBody({
pathName1: pathvalue1,
pathName2: pathvalue2,
pathName3: pathvalue3,
}, {
queryName1: queryvalue1,
queryName2: queryvalue2,
}, body);
If you set the prefix of the api, you have to set the $baseURL
of client api.
api.$baseURL = '/same/as/the/prefix/in/server';
FullDate
If the format is string
date
, you should use FullDate
instead of Date
.
FullDate
is a wrapper of Date
, which implements .toString()
, .toJSON()
and .valueOf()
to make it more convenience to convert it to String or JSON.
Import FullDate
class from @supmiku39/api-ts-gen/utils
.
import {FullDate} from '@supmiku39/api-ts-gen/utils';
// initialization
new FullDate(new Date()); // from a Date instance
new FullDate(); // today
new FullDate('2012-03-31'); // 2012-03-31
new FullDate(2015, 5); // 2015-05-01
new FullDate(2015, 5, 4); // 2015-05-04
new FullDate(1449446400000); // 2015-12-07
// getter
const d = new FullDate();
d.date // a Date instance clone
d.year // date.getUTCFullYear()
d.month // date.getUTCMonth()+1
d.day // date.getUTCDate()
d.dayOfWeek // date.getUTCDay()
// setter
d.year = 2018; // date.setUTCFullYear(2018)
d.month = 3; // date.setUTCMonth(3-1)
d.day = 5; // date.setUTCDate(5)
// method
/*
.advance(period: number): FullDate
return a new FullDate that advanced by $peroid days
*/
const d0 = new FullDate('2015-05-04');
d0.advance(217) // 2015-12-07
d0.advance(-1129) // 2012-03-31
/*
.distanceFrom(d0: FullDate): number
return the distance(unit: day) from d0
*/
d0.distanceFrom(new FullDate(2018, 3, 5)); // -1036
d0.distanceFrom(new FullDate(2007, 8, 31)); // 2803
⚠️ FullDate
use month
from 1 to 12, which differs from Date
. Also, FullDate
use day
and dayOfWeek
instead of date
and day
.
Schema
It is not necessary to use SomeSchema.from(...)
of SomeSchema.Partial(...)
to create an instance to pass to the client api. Instead, simply use an object literal.
Import the schema type interfaces from #api/schemas
.
import {SchemaA, SchemaB} from '#api/schemas';
api.postA({id: 3}, {...}) // OK, simpler
api.postA({id: 3}, SchemaA.from(...)); // Well, still OK
api.patchB({id: 3}, {...}) // OK, simpler
api.patchB({id: 3}, SchemaB.Partial(...)); // Well, still OK
⚠️ From v2.0.0, Schemas
are no longer class. Instead, it became a interface
and a Object including from
, Partial
, and fields
properties.
Handling response
The api is a async function that returns a APIPromise
, which is a extended Promise. The promise is resolved if the status code is 2xx
, otherwise it is rejected.
Every operation has a different APIPromise
definition according to the possible response status code.
If there is only one 2xx
in the status code and error handling is not required, you can simply use await
to get the response body.
const a: SchemaA = await api.getA(...);
However, if you want to handle several kinds of response, you can use .on(statusCode, responseBody => handler)
where XXX
is the status code.
api.getA(...)
.on(200, a => { // the compiler knows that type of `a` is `SchemaA`
console.log('Get a successfully', a);
})
.on(404, msg => {
console.log('Not found with msg', msg);
})
.on(403, () => {
console.log('Forbidden with no msg');
});
⚠️ For any status, you can only call .on(status, ...)
once.
// OK
api.getA(...)
.on(200, a => ...)
.on(403, () => ...)
// NG
api.getA(...)
.on(200, a => ...)
.on(200, () => ...) // Compile Error!
Details
Type Conversion
OpenAPI data types will be convert to TypeScript types as following:
type | format | TypeScript | |
---|---|---|---|
integer |
→ | number |
|
number |
→ | number |
|
string |
→ | string |
|
boolean |
→ | boolean |
|
string |
date |
→ | FullDate |
string |
date-time |
→ | Date |
In addition, assigning nullable: true makes it nullable. |
Strict Type Parser(STP)
To ensure type safety, STP
provides some static functions to convert any
type to specific type.
type | format | valid type | note |
---|---|---|---|
integer |
int32 |
number , string |
integer between -2147483648 and 2147483647 only |
integer |
int64 |
number , string |
no check for range and floating point |
number |
number , string |
NaN and Infinity is valid |
|
string |
string |
||
boolean |
boolean , string |
||
string |
date |
FullDate , Date , number , string |
|
string |
date-time |
Date , number , string |
|
string |
byte |
string , Buffer |
no validation check |
string |
binary |
string , Buffer |
no validation check |
In addition, assigning nullable: true makes it nullable. |
// integer.int32
39 // OK
'39' // OK
'1e3' // NG, no scientific notation
3.14 // NG, not a integer
2147483648 // NG, overflow
'1e100' // NG, overflow
NaN // NG, not a integer
Infinity // NG, not a integer
'pi' // NG, not a number
// integer.int64, number
39 // OK
'39' // OK
'1e3' // OK
3.14 // OK, although it is convertible to int64
2147483648 // OK
'1e100' // OK, although too big for int64
NaN // OK, although it is convertible to int64
Infinity // OK, although it is convertible to int64
'pi' // NG, not a number
// boolean
true, false // OK
'true', 'false' // OK
'T', 'F' // NG, only 'true' and 'false' are valid
0, 1 // NG, same reason
// FullDate
new FullDate() // OK
new Date() // OK
'2017-05-28' // OK
1449446400000 // OK, 2015-12-07
'today' // NG, not a valid Date
// Date
new Date() // OK
'2017-05-28' // OK
1449446400000 // OK, 2015-12-07T00:00:00.000Z
new FullDate() // NG
'today' // NG, not a valid Date
// string.byte
'5aeJ5qeY44GL44KP44GE44GE' // OK
'********' // Well, OK, although invalid for a base64 string
Buffer.from('nya') // OK, a Buffer(node.js)
0x52391207 // NG, number is invalid
new Blob([]) // NG, Blob is not supported
// string.
'e5a789e6a798e3818be3828fe38184e38184' // OK
'********' // Well, OK, although invalid for a hex string
Buffer.from('nya') // OK, a Buffer(node.js)
0x52391207 // NG, number is invalid
new Blob([]) // NG, Blob is not supported
Schema
Base on #/components/schemas
, it generates interface definitions and constructor functions in schemas.ts
.
Interface Definition
For example,
Post:
type: "object"
properties:
id:
type: "integer"
format: "int32"
ts:
type: "string"
format: "date-time"
authorID
type: "integer"
format: "int32"
content:
type: "string"
nullable: true
images:
type: "array"
items:
type: "string"
format: "binary"
pinned:
type: "boolean"
will become
interface Post {
id: number;
ts: string;
authorID: number;
content: string | null;
images: string[];
pinned: boolean;
}
Constructor Function
It also generates constructor function Schema.from
with strict type checking.
The constructor function takes exactly one argument with literal object type.
If any property of the argument is not convertible to expected type, it throws an BadValueError
.
For example,
NamedCircle:
type: "object"
properties:
name:
type: "string"
radius:
type: "number"
format: "double"
color:
type: "string"
nullable: true
will become
interface NamedCircle {
name: string;
radius: number;
color: string | null;
}
Here are some examples for strict type checking:
NamedCircle.from({
name: 'red circle',
radius: 39,
color: 'red',
}); // OK
NamedCircle.from({
name: 'circle with null color',
radius: 0,
color: null,
}); // OK, color is nullable
NamedCircle.from({
name: 'circle with null color',
radius: 0,
color: undefined,
}); // Error! color should be a number or null
NamedCircle.from({
name: 'circle without given color',
radius: 0,
}); // Error! color should be given
NamedCircle.from({
name: 'circle with invalid radius',
radius: 'miku',
color: 'cyan',
}); // Error! radius should be a number
Partial Function
It also generates a function called Partial
for initializing Partial type.
The function also takes exactly one argument.
If any property of the argument is absent or is undefined, the function will skip setting the property.
However, if the property is given but not convertible to expected type, it throws a BadValueError
.
Here are some examples.
NamedCircle.Partial({
name: 'circle with radius 39',
radius: 39,
color: 'cyan',
}); // OK
NamedCircle.Partial({
name: 'circle without radius and color',
}); // OK
NamedCircle.Partial({
radius: 1207,
}); // OK
NamedCircle.Partial({}); // OK
NamedCircle.Partial({
radius: 1207,
color: null,
}); // OK, color can be null
NamedCircle.Partial({
name: undefined,
radius: 1207,
color: null,
}); // OK, just skip setting name
NamedCircle.Partial({
name: null,
radius: 'miku',
color: 'cyan',
}); // Error! name is not nullable
NamedCircle.Partial({
radius: 'miku',
color: 'cyan',
}); // Error! radius should be a number
⚠️ Use object literal if possible. The from
and Partial
function are mainly for internal use, avoid to use them unless you want to safe and strict convert a any variable to a schema type.
fields
Schema
exposes its fields to fields
constant.
Its type is Array<keyof Schema>
.
NamedCircle.fields // ['name', 'radius', 'color']: Array<keyof NamedCircle>
Limitations
application/json only
This tool only supports application/json
type for request and response body. Any other type like multipart/form
or image/*
are not supported and will be ignored.
schema $ref only
Other $ref like requestBody, responseBody are not supported currently.
Versions
2.0.0
- simplify generated code
- merge all APIPromise class
- remove IServerAPI and IClientAPI
- remove res Object, return [status, body] in ServerAPI instead
- remove schema classes, use interface instead
-s
flag forctx.state
interface path
1.1.3
- expose fields of schemas to XXX.fields(static variable)
1.1.2
- publish to npmjs and change the package name in generated code
- specify constructor argument type of FullDate
1.1.1
- implement FullDate#distanceFrom(d0)
- fix FullDate timezone bug, use UTC instead
1.1.0
- fix Partial constructor, enhance error msg
- add int32 STP
1.0.1
- implement FullDate getter, setter, function(advance)
1.0.0
- ClientAPI, ServerAPI interface, Schema TS codegen implemented
- application/json only
- get, post, put, delete, patch only