mnahkies / openapi-code-generator

A code generation tool for openapi 3 / 3.1 specifications written in typescript, primarily aimed at generating typescript clients and server stubs. Other target languages may be added in future.
https://openapi-code-generator.nahkies.co.nz/
MIT License
20 stars 2 forks source link

Embedding spec into generated code #102

Open ADRFranklin opened 11 months ago

ADRFranklin commented 11 months ago

This should definitely be it's own issue (coming from this issue) and this shouldn't be strictly related to #81.

So I have used other projects (mainly golang related) for openapi, and I was spoiled with the option of having the spec embedded into the server via base64 encoding. I'll provide this link here, that gives a good example of how that may look.

Unlike issue #81 this is not about providing documentation, but rather exposing the openapi schema itself, which can be served by the application itself, allowing someone to generate a client from that schema for the exact version of software that is hosting it. I've personally always found this way of doing it to be fitting for almost all apps that I have written using openapi.

Now I am not sure how involved you want this process to be, I personally would be fine with having it generated and a few helper functions to decode it, cache it, and then provide a function to get the cached contents of which could be served on any endpoint I wish without any forced requirements. However maybe you want the process to be more controlled, and maybe you have some function that takes an endpoint string, and then the library handles serving it up on that endpoint, with very little effort on the developer using it.

I hope this is something that makes it into the library in some way, as I see it being very beneficial.

mnahkies commented 11 months ago

I definitely agree that it generally makes sense for applications to expose the schema they were built with on an endpoint (and generally I expect them to additionally expose rendered docs using redoc / whatever)

With regards to #81 I was imagining something that would act as a convenience method for doing this, where you could optionally provide a route to expose the schema and rendered docs.

I'm not 100% sure what that interface should look like yet, or when I'll get to it though.

The way I currently tend to structure my projects looks something like:

├── Dockerfile
├── openapi.yaml
├── package.json
├── README.md
├── src
│   ├── config.ts
│   ├── database/
│   ├── generated.ts
│   ├── index.spec.ts
│   ├── index.ts
│   ├── middleware/
│   ├── models.ts
│   ├── routes/
│   └── schemas.ts
├── tsconfig.json
└── yarn.lock

And I'd include the openapi.yaml in my docker image, making it readily available to serve as a static file at runtime.

With the revised approach of providing a router parameter to bootstrap, you could take a similar approach and mount an additional route on it easily enough, in the simplest instance something like:

const router = createRouter(...)
router.get('/openapi.yaml', ctx => {
  ctx.body = fs.readFileSync('./openapi.yaml', 'utf-8')
  ctx.status = 200
})
bootstrap({router, ...})

(adjust route / filepath / read file content once at startup / etc as appropriate)

The other approach I've taken in the past has been to have a centralized/mono repository of openapi definitions such that the content of the definition gets pulled at the time of generation. In this instance I'd probably be inclined to just curl it to a file still and commit it.

In the golang example you linked, I imagine the motivation for embedding as a base64 string is related to wanting to have a single static binary to ship (though I think using the recent stdlib addition https://pkg.go.dev/embed would make more sense now - you can still do stuff like only gzip it once if you really want, though I'd be surprised if the endpoint was getting called often enough to need to bother).

Are you doing some kind of bundling or making use of https://nodejs.org/api/single-executable-applications.html that makes embedding the definition into the source code particularly desirable?

Generally I think it works out better in terms of VCS history to keep it as a separate flat file if possible.

ADRFranklin commented 11 months ago

I will be making use of pkg (https://github.com/vercel/pkg) in order to ship the app as a single binary, so it would make sense in this case, and I haven't yet but would probably make use of the same system for docker, depends on the how big the image ends up being and if there is need to make it smaller, as the pkg can be pretty big on their own.

I generally store the openapi file in a folder in the root of the project so /api/openapi/openapi.yaml and then I use that to generate both frontend client and the backend server.

As for golang, I agree it's mostly done for portability, though I wonder if it's also done to reduce IO access as in not having to read a file and always have it in memory and cached, for quick access.

What's your thoughts on the format that is served? I have come across a few projects that seem to only support generating clients from json but not the yaml format, so I always wondered if it was best served in json as it is more popular. The golang project I linked before, also seems to serve it as json, and I wondered what the reason for that was, I know golang has good json support already, so it could of just been to not have to have another third party dependency.

mnahkies commented 11 months ago

In the case of using pkg, would this be enough? https://github.com/vercel/pkg#detecting-assets-in-source-code

Had a quick go at testing it out (was actually considering using pkg for something anyway) but found that nodejs 20 isn't supported and gave up.

Eg: here's an implementation serving the spec + redoc from one of my projects

// ./src/withDocs.ts

import KoaRouter from "@koa/router"
import * as fs from "node:fs"
import path from "node:path"

export function withDocs(router: KoaRouter): KoaRouter {
    router.get("/openapi.yaml", (ctx) => {
        ctx.body = fs.readFileSync(path.join(__dirname, "../openapi.yaml"))
        ctx.status = 200
    })

    router.get("/docs", (ctx) => {
        ctx.body = `
<!DOCTYPE html>
<html>
  <head>
    <title>Docs</title>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
    <style>
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <redoc spec-url='./openapi.yaml'></redoc>
    <script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha384-R8e5ippgVo+kphHRsZE026R4rLIN/ORakEnRnOJ3S7BauiXHeD2EnvDpCcPYV4O/" crossorigin="anonymous"></script>
  </body>
</html>
    `
        ctx.status = 200
    })

    return router
}

What's your thoughts on the format that is served? I have come across a few projects that seem to only support generating clients from json but not the yaml format, so I always wondered if it was best served in json as it is more popular.

I'd probably ideally support serving either .json or .yaml tbh, though it'd be a bit annoying to have to ship a yaml parser as a runtime dependency just for this.

Most tools that consume this stuff tend to be fine with either though in my experience so might not matter too much.

though I wonder if it's also done to reduce IO access as in not having to read a file and always have it in memory and cached, for quick access.

I'd expect the Linux filesystem cache to basically already be doing this, and honestly can't think of many use cases for a highly optimized endpoint to retrieve the spec, as I'd generally expect it to receive very low traffic.