huma icon indicating copy to clipboard operation
huma copied to clipboard

How to separate internal and external API routes?

Open cplaetzinger opened this issue 1 year ago • 6 comments

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

cplaetzinger avatar Sep 25 '24 07:09 cplaetzinger

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.

superstas avatar Oct 01 '24 15:10 superstas

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.

cplaetzinger avatar Oct 01 '24 17:10 cplaetzinger

@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.

superstas avatar Oct 01 '24 19:10 superstas

Many thanks! I'll try that.

cplaetzinger avatar Oct 06 '24 17:10 cplaetzinger

@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 avatar Oct 09 '24 00:10 danielgtaylor

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.

cardinalby avatar Nov 09 '24 22:11 cardinalby