openapi-backend
openapi-backend copied to clipboard
Generate TS types for params/response for each operation?
Hi,
I'm considering this tool for a project, comparing it to GraphQL or django-rest-framework in python.
I'd like the types described in my oas to be enforced in my application code with TypeScript. For example, a generate command might produce code like this:
type GetPetsParams = {
status?: 'available' | 'unavailable',
limit?: number,
}
type type GetPetParams = {
id: string,
}
type Pet = {
id: string,
status: 'available' | 'unavailable',
name: string,
}
Thoughts?
You can use tools like openapi-typescript or dtsgenerator to establish this. :)
Feel free to suggest ways to integrate these tools into openapi-backend
Good question…
As I think about it, I think you would need a framework-specific shim to be able to use the types in nice ways. Ultimately it would be great to generate a type like this, using openapi-typescript as an example:
import {operations} from './openapi-typescript-generated'
// generated code:
type Operations = {
getPets: (query: operations['getPets']['parameters']['query'], req: ExpressRequest, rsp: ExpressResponse) => operations['getPets']['responses'][200],
updatePet: (
petId: operations['updatePet']['parameters']['path']['pet_id'],
body: operations['updatePet']['requestBody']['application/json'],
req: ExpressRequest,
rsp: ExpressResponse,
) => operations['updatePet']['responses'][200],
}
then in your application you would write:
import {Operations} from './openapi-backend-generated';
const operations: Operations = {
getPets: async (query) => {
const { status } = query; // typed!
const pets = await petdb.getByStatus(status);
return {pets} // types enforce that this matches your schema
},
updatePet: async (petId, body, req) => {
authenticate(req.headers);
const {status, description} = body; // typed!
const result = await petdb.update(petId, {status, description});
return result; // typechecked to match schema
}
}
api.registerExpressHandlers(operations);
and TS would tell you if you forget to implement a method, return the wrong 200 response type, access a param that does not exist, etc etc.
I am not sure if this makes the most sense to take place as a part of this package, or as a new package like openapi-backend-express. It might be most convenient if it bundled openapi-typescript and ran the generation of the operation method signatures in the same step. But it also needs a runtime shim for nice params/return types, and to pass the args in the right places depending on the openapi spec (eg, path params individually, then query param, then body param, omitting any that do not exist for the method, followed by framework-specific context).
What do you think?
@rattrayalex
you can do something like this:
// openapi-backend-generated generated from openapi-typescript package
import { operations } from './openapi-backend-generated';
// define api
const api = new OpenAPIBackend({
definition: path.join(__dirname, '..', 'openapi.yml'),
handlers: {
getProfile: async (c, req: Request, res: Response) => {
type response = operations['getProfile']['responses']['200']['content']['application/json'];
// use response type ...
return res.status(200).json({ operationId: c.operation.operationId });
},
validationFail: async (c, req: Request, res: Response) => res.status(400).json({ err: c.validation.errors }),
notFound: async (c, req: Request, res: Response) => res.status(404).json({ err: 'not found' }),
},
});
Editing generated code isn't a great idea in my experience… this also wouldn't offer typechecked params, doesn't enforce typechecking the response (or do so automatically) and feels fairly noisy.
I implemented this with openapi-typescript. If you import the operations interface from the generated types, you can pass in handlers.
import { operations } from '../definition';
const handlers = createHandlers<operations>({
// handler definitions here...
});
The current version uses Express types, but can be easily modified for other libraries.
import { Request, Response } from 'express';
import { Context, Handler, ParsedRequest } from 'openapi-backend';
type Operation = {
parameters?: {
path?: any;
query?: any;
};
responses: Record<string, any>;
};
type OperationPath<T> = T extends {
parameters?: {
path?: infer U;
};
}
? U
: never;
type OperationQuery<T> = T extends {
parameters?: {
query?: infer U;
};
}
? U
: never;
type OperationBody<T> = T extends {
requestBody?: {
content?: {
'application/json': infer U;
};
};
}
? U
: never;
type JsonResponse<T> = T extends {
content?: {
'application/json': infer U;
};
}
? U
: any;
type OperationResponse<T> = T extends {
responses?: Record<string, infer U>;
}
? JsonResponse<U>
: any;
interface ExtendedRequest<
TParams extends Record<string, string | string[]>,
TQuery extends Record<string, string | string[]>,
TBody
> extends ParsedRequest {
params: TParams;
cookies: {
[key: string]: string | string[];
};
query: TQuery;
requestBody: TBody;
body: TBody;
}
interface ExtendedContext<
TParams extends Record<string, string | string[]>,
TQuery extends Record<string, string | string[]>,
TBody,
TResponse
> extends Context {
request: ExtendedRequest<TParams, TQuery, TBody>;
response: Response<TResponse>;
}
type ExtendedHandler<
TParams extends Record<string, string | string[]>,
TQuery extends Record<string, string | string[]>,
TBody,
TResponse
> = (
context: ExtendedContext<TParams, TQuery, TBody, TResponse>,
request: Request,
response: Response<TResponse>
) => any | Promise<any>;
type OperationHandler<T> = T extends Operation
? ExtendedHandler<OperationPath<T>, OperationQuery<T>, OperationBody<T>, OperationResponse<T>>
: any;
export type Handlers<T> = {
[Property in keyof T]: OperationHandler<T[Property]>;
};
export function createHandlers<TOperations>(handlers: Handlers<TOperations>) {
return handlers as unknown as Record<string, Handler>;
}
@sjohnsonaz this is outstandingly helpful. It would be great to see this in a readme or something, as it was basically the missing piece of my OAS setup. That said, to anyone else who might land here, the ExtendedHandler should be
type ExtendedHandler<
TParams extends Record<string, string[] | string>,
TQuery extends Record<string, string[] | string>,
TBody,
TResponse
> = (
context: ExtendedContext<TParams, TQuery, TBody, TResponse>,
request: ExtendedRequest<TParams, TQuery, TBody>,
response: Response<TResponse>
) => Promise<any> | any;
For type safety on the request object as well