Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to separate internal and external API routes? #588

Open
cplaetzinger opened this issue Sep 25, 2024 · 6 comments
Open

How to separate internal and external API routes? #588

cplaetzinger opened this issue Sep 25, 2024 · 6 comments
Labels
question Further information is requested

Comments

@cplaetzinger
Copy link

Hi there,

is there a way to generate two different versions of the OpenAPI specification and distinguish between internal and external routes? Our idea is that we want only specific routes to be included in the documentation we provide to some of our clients. Some other routes should not appear there at all because they are only for internal use by us. Any ideas on how this can be archived?

Many thanks
Christian

@superstas
Copy link
Contributor

Hey @cplaetzinger

I think, you can achieve it by setting Hidden: true for each Operation you want to skip in the generated OAS.

https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation

	// Hidden will skip documenting this operation in the OpenAPI. This is
	// useful for operations that are not intended to be used by clients but
	// you'd still like the benefits of using Huma. Generally not recommended.

@cplaetzinger
Copy link
Author

Hey @cplaetzinger

I think, you can achieve it by setting Hidden: true for each Operation you want to skip in the generated OAS.

https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation

	// Hidden will skip documenting this operation in the OpenAPI. This is
	// useful for operations that are not intended to be used by clients but
	// you'd still like the benefits of using Huma. Generally not recommended.

Thanks for your input. We're aware of this flag but as it will hide these endpoints completely. We want to generate two api documentations which are based on the same source but contain different endpoints.

@superstas
Copy link
Contributor

superstas commented Oct 1, 2024

@cplaetzinger, if I understand your idea, this is probably one way to have the only source and two different specs.

Pseudo API with four endpoints.
You can control which of the operations would be skipped in which specs.

In this example, there are:

  • "Public API" with only two operations ( "GetUser", "UpdateUser")
  • "Private API" with the full list of operations from the source.

The diff between yaml files:

5a6,12
>         $schema:
>           description: A URL to the JSON Schema for this object.
>           examples:
>             - https://example.com/schemas/CreateUserOutputBody.json
>           format: uri
>           readOnly: true
>           type: string
13a21,27
>         $schema:
>           description: A URL to the JSON Schema for this object.
>           examples:
>             - https://example.com/schemas/DeleteUserOutputBody.json
>           format: uri
>           readOnly: true
>           type: string
109c123
<   title: Public API
---
>   title: Private API
112a127,142
>   /user:
>     post:
>       operationId: CreateUser
>       responses:
>         "200":
>           content:
>             application/json:
>               schema:
>                 $ref: "#/components/schemas/CreateUserOutputBody"
>           description: OK
>         default:
>           content:
>             application/problem+json:
>               schema:
>                 $ref: "#/components/schemas/ErrorModel"
>           description: Error
113a144,164
>     delete:
>       operationId: DeleteUser
>       parameters:
>         - in: path
>           name: id
>           required: true
>           schema:
>             type: string
>       responses:
>         "200":
>           content:
>             application/json:
>               schema:
>                 $ref: "#/components/schemas/DeleteUserOutputBody"
>           description: OK
>         default:
>           content:
>             application/problem+json:
>               schema:
>                 $ref: "#/components/schemas/ErrorModel"
>           description: Error

Example

package main

import (
	"context"
	"net/http"
	"os"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
	"github.com/go-chi/chi/v5"
)

type CreateUserInput struct{}
type CreateUserOutput struct {
	Body struct {
		Message string `json:"message"`
	}
}

type GetUserInput struct {
	ID string `path:"id" required:"true"`
}

type GetUserOutput struct {
	Body struct {
		Message string `json:"message"`
	}
}

type UpdateUserInput struct {
	ID string `path:"id" required:"true"`
}

type UpdateUserOutput struct {
	Body struct {
		Message string `json:"message"`
	}
}

type DeleteUserInput struct {
	ID string `path:"id" required:"true"`
}

type DeleteUserOutput struct {
	Body struct {
		Message string `json:"message"`
	}
}

// Function to add routes with the ability to hide specific operations
func addRoutes(api huma.API, hiddenOperations []string) {
	isHidden := func(operationID string) bool {
		for _, id := range hiddenOperations {
			if id == operationID {
				return true
			}
		}
		return false
	}

	// CreateUser route
	huma.Register(api, huma.Operation{
		OperationID: "CreateUser",
		Method:      http.MethodPost,
		Path:        "/user",
		Hidden:      isHidden("CreateUser"),
	}, func(ctx context.Context, input *CreateUserInput) (*CreateUserOutput, error) {
		resp := &CreateUserOutput{}
		resp.Body.Message = "CreateUser works!"
		return resp, nil
	})

	// GetUser route
	huma.Register(api, huma.Operation{
		OperationID: "GetUser",
		Method:      http.MethodGet,
		Path:        "/user/{id}",
		Hidden:      isHidden("GetUser"),
	}, func(ctx context.Context, input *GetUserInput) (*GetUserOutput, error) {
		resp := &GetUserOutput{}
		resp.Body.Message = "GetUser with ID: " + input.ID + " works!"
		return resp, nil
	})

	// UpdateUser route
	huma.Register(api, huma.Operation{
		OperationID: "UpdateUser",
		Method:      http.MethodPut,
		Path:        "/user/{id}",
		Hidden:      isHidden("UpdateUser"),
	}, func(ctx context.Context, input *UpdateUserInput) (*UpdateUserOutput, error) {
		resp := &UpdateUserOutput{}
		resp.Body.Message = "UpdateUser with ID: " + input.ID + " works!"
		return resp, nil
	})

	// DeleteUser route
	huma.Register(api, huma.Operation{
		OperationID: "DeleteUser",
		Method:      http.MethodDelete,
		Path:        "/user/{id}",
		Hidden:      isHidden("DeleteUser"),
	}, func(ctx context.Context, input *DeleteUserInput) (*DeleteUserOutput, error) {
		resp := &DeleteUserOutput{}
		resp.Body.Message = "DeleteUser with ID: " + input.ID + " works!"
		return resp, nil
	})
}

func newAPI(name, version string, hiddenOperations []string) huma.API {
	router := chi.NewMux()
	cfg := huma.DefaultConfig(name, version)
	api := humachi.New(router, cfg)
	addRoutes(api, hiddenOperations)
	return api
}

func saveAPI(api huma.API, filename string) {
	spec, err := api.OpenAPI().YAML()
	if err != nil {
		panic(err)
	}

	if err := os.WriteFile(filename, spec, 0644); err != nil {
		panic(err)
	}
}

func main() {
	publicAPI := newAPI("Public API", "1.0.0", []string{"DeleteUser", "CreateUser"})
	privateAPI := newAPI("Private API", "1.0.0", []string{})

	saveAPI(publicAPI, "public-api.yaml")
	saveAPI(privateAPI, "private-api.yaml")
}

I hope that makes sense for you.

@cplaetzinger
Copy link
Author

Many thanks! I'll try that.

@danielgtaylor
Copy link
Owner

danielgtaylor commented Oct 9, 2024

@cplaetzinger let me know if that works for you. @superstas thanks for the help! BTW in the past I also used something like https://github.com/danielgtaylor/apiscrub and just added some extensions into the OpenAPI to mark which operations were private, then had a separate step to publish both OpenAPI documents. I do like the idea of doing it all in-process and with Huma though!

BTW this is a common enough ask I've gotten that I'm open to ideas for how to make this easier.

@danielgtaylor danielgtaylor added the question Further information is requested label Oct 9, 2024
@cardinalby
Copy link

I believe we should make it possible without modifying the client code that registers the operations but redirecting the registration calls to the separate instances of OpenAPI instead depending on provided "api" instance.

Having "derived groups" concept, we can provide different api objects to register "internal" and "external" endpoint without changes in endpoint definitions. This way we can route registration to a separate OpenAPI instances and expose separate "spec" and "schema" endpoints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants