honojs / middleware

monorepo for Hono third-party middleware/helpers/wrappers
https://hono.dev
412 stars 142 forks source link

Swagger middleware #95

Open vincent-vade opened 1 year ago

vincent-vade commented 1 year ago

Hi,

Hono is awesome but is it possible to add swagger middleware ?

Best regards

Vincent

yusukebe commented 1 year ago

Hi @vincent-vade!

Yes, creating a middleware for Swagger sounds like a good idea. Ideally, we would create an OpenAPI middleware, but we're still in the planning phase since we haven't started the API design yet. However, if you're talking about implementing just the Swagger UI, it might be simpler to accomplish.

If you're referring to the full suite of OpenAPI tools, we would need to start designing the middleware. For example, we would need to decide how to define schemas, whether it be with YAML, JSON, or a JS code base.

kiancyc commented 1 year ago

Hope we can have an auto openapi middleware like Hapi.js Hapi generates openapi doc by Joi validator. Hono already has zod validator middleware. https://github.com/asteasolutions/zod-to-openapi

yusukebe commented 1 year ago

"zod-to-openapi" looks good. I'll check it in more detail later.

ZerNico commented 1 year ago

I think it'd be a good idea to also allow support for different validation libraries, so a modular design would be good. Maybe a general combination with the validation middlewares to just extract the schema from them and a mapper for validation lib to openapi schema

yusukebe commented 1 year ago

@kiancyc

Is Hapi's generator this?: https://github.com/hapi-swagger/hapi-swagger

@ZerNico

Yeah, it's great that we can support Zod, Valibot, Joi, or others. Hono's validator (though the design might not be the best) can support any validator. So, we have Zod, Valibot, and TypeBox validators. If we implement something like that, we can handle any validator, and it can generate OpenAPI docs.

yusukebe commented 1 year ago

I've tried "zod-to-openapi" and a new feature for the validator.

Even though it's not released yet, I've made the zod validator capable of receiving the schema argument to validate response types (just types) as the third argument:

const apiRoutes = api.get('/users/:id', zValidator('param', ParamsSchema, UserSchema), (c) => {
  const { id } = c.req.valid('param');
  //
  return c.jsonT({
    id: '123',
    name: 'foo',
    age: 30,
  });
})

UserSchema will validate response types, which are rewritten in jsonT(). Perhaps it should validate actual values, but for now, it validates types only.

Next, I created the schema using zod-to-openapi:

import type { RouteConfig } from '@asteasolutions/zod-to-openapi'
import { extendZodWithOpenApi, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
import { z } from 'zod'

extendZodWithOpenApi(z)

const registry = new OpenAPIRegistry()

const ParamsSchema = z.object({
  id: registry.registerParameter(
    'UserId',
    z.string().openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    })
  ),
})

const UserSchema = z
  .object({
    id: z.string().openapi({
      example: '1212121',
    }),
    name: z.string().openapi({
      example: 'John Doe',
    }),
    age: z.number().openapi({
      example: 42,
    }),
  })
  .openapi('User')

const route = {
  method: 'get',
  path: '/users/:id',
  description: 'Get user data by its id',
  summary: 'Get a single user',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      description: 'Object with user data.',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
} as RouteConfig

registry.registerPath(route)

export { ParamsSchema, UserSchema, registry }

Then, it can return the docs as JSON from an endpoint:

app.get('/schema', prettyJSON(), (c) => {
  const generator = new OpenApiGeneratorV3(registry.definitions)
  const document = generator.generateDocument({
    openapi: '3.0.0',
    info: {
      version: '1.0.0',
      title: 'My API',
    },
    servers: [{ url: 'v1' }],
  })
  return c.json(document)
})

Result:

SS

Of course, you can generate it as a YAML file as well.

This looks good, and we can use it in the way described above already. However, we can make it more like an application.

kiancyc commented 1 year ago

@kiancyc

Is Hapi's generator this?: https://github.com/hapi-swagger/hapi-swagger

@ZerNico

Yeah, it's great that we can support Zod, Valibot, Joi, or others. Hono's validator (though the design might not be the best) can support any validator. So, we have Zod, Valibot, and TypeBox validators. If we implement something like that, we can handle any validator, and it can generate OpenAPI docs.

Yes, it is.

kiancyc commented 1 year ago

I can give two more web framework examples that has openai doc function buil-in:

Nest.JS TS FastApi Python

They all have openapi integration out of box.

kiancyc commented 1 year ago

I've tried "zod-to-openapi" and a new feature for the validator.

Even though it's not released yet, I've made the zod validator capable of receiving the schema argument to validate response types (just types) as the third argument:

const apiRoutes = api.get('/users/:id', zValidator('param', ParamsSchema, UserSchema), (c) => {
  const { id } = c.req.valid('param');
  //
  return c.jsonT({
    id: '123',
    name: 'foo',
    age: 30,
  });
})

UserSchema will validate response types, which are rewritten in jsonT(). Perhaps it should validate actual values, but for now, it validates types only.

Next, I created the schema using zod-to-openapi:

import type { RouteConfig } from '@asteasolutions/zod-to-openapi'
import { extendZodWithOpenApi, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'
import { z } from 'zod'

extendZodWithOpenApi(z)

const registry = new OpenAPIRegistry()

const ParamsSchema = z.object({
  id: registry.registerParameter(
    'UserId',
    z.string().openapi({
      param: {
        name: 'id',
        in: 'path',
      },
      example: '1212121',
    })
  ),
})

const UserSchema = z
  .object({
    id: z.string().openapi({
      example: '1212121',
    }),
    name: z.string().openapi({
      example: 'John Doe',
    }),
    age: z.number().openapi({
      example: 42,
    }),
  })
  .openapi('User')

const route = {
  method: 'get',
  path: '/users/:id',
  description: 'Get user data by its id',
  summary: 'Get a single user',
  request: {
    params: ParamsSchema,
  },
  responses: {
    200: {
      description: 'Object with user data.',
      content: {
        'application/json': {
          schema: UserSchema,
        },
      },
    },
  },
} as RouteConfig

registry.registerPath(route)

export { ParamsSchema, UserSchema, registry }

Then, it can return the docs as JSON from an endpoint:

app.get('/schema', prettyJSON(), (c) => {
  const generator = new OpenApiGeneratorV3(registry.definitions)
  const document = generator.generateDocument({
    openapi: '3.0.0',
    info: {
      version: '1.0.0',
      title: 'My API',
    },
    servers: [{ url: 'v1' }],
  })
  return c.json(document)
})

Result:

SS

Of course, you can generate it as a YAML file as well.

This looks good, and we can use it in the way described above already. However, we can make it more like an application.

I think the tricky part is to auto generate the RouteConfig.

kiancyc commented 1 year ago

Express.js joi and swagger integration middleware:

https://github.com/vforv/express-joi-simple

import * as express from 'express';
import * as joi from 'joi';
import * as BodyParser from 'body-parser';
import { Doc, Validate, RequestHandler } from 'express-joi-simple';

const app = express();
app.use(BodyParser.json());

const schema = {
    body: {
        test1: joi.string().required()
    },
    model: 'Register'
}

// Note middleware here
app.post('register', Validate(schema), (req: any, res: any) => {
    res.json({
        message: 'register'
    })
})

app.use(RequestHandler);

app.listen(3000, () => {
    // Note function Doc here
    Doc(app);
})
ZerNico commented 1 year ago

Express.js joi and swagger integration middleware:

https://github.com/vforv/express-joi-simple

import * as express from 'express';
import * as joi from 'joi';
import * as BodyParser from 'body-parser';
import { Doc, Validate, RequestHandler } from 'express-joi-simple';

const app = express();
app.use(BodyParser.json());

const schema = {
    body: {
        test1: joi.string().required()
    },
    model: 'Register'
}

// Note middleware here
app.post('register', Validate(schema), (req: any, res: any) => {
    res.json({
        message: 'register'
    })
})

app.use(RequestHandler);

app.listen(3000, () => {
    // Note function Doc here
    Doc(app);
})

This extracts the schema by calling the route with "schemaBypass" as the req parameter. The joi middlware then just returns the schema instead of doing validation. I'm not sure if that is the best way to handle something like that, it stops working if there is any other middleware infront of it that returns something or throws. But I also can't think of a good way to access the schema. Maybe attaching it to the returned Middleware function kind of like how displayName and such is set in react?

Also instead of modifying (or wrapping) the validation library zod-to-openapi does it'd probably be better to have another parameter to set the example object and just check the type of that with the passed schema. Would make it easier to use with different libs.

This goes very deep into Hono then, would a plugin API make more sense to accomplish something like this?

rafaell-lycan commented 11 months ago

@yusukebe should this issue be closed or converted to a discussion?

yusukebe commented 11 months ago

@rafaell-lycan

We don't have to close.

Do you mean a "discussion" is one of a GitHub Discussion?

popuguytheparrot commented 11 months ago

Hello. Since we are discussing open api here, I would like to point out another solution https://github.com/jlalmes/trpc-openapi that requires integration of both swagger and hono.