Skip to content

Feature Proposal: Add Generic Register[Req, Res] Helper for Cleaner Handler + Spec Binding🚀 #21

@derkan

Description

@derkan

Hi 👋

First of all — thank you for this awesome package! It's clean, intuitive, and makes OpenAPI integration much easier in Go web projects. 🙌

Problem

When defining routes using .With(...), we need to manually specify the request and response types, even though our handler already "knows" them.

Example:

v1.Get("/users/{id}", GetUserHandler).With(
    option.Summary("Get user by ID"),
    option.Request(new(GetUserRequest)),
    option.Response(200, new(User)),
)

This leads to a bit of repetition and can become error-prone in larger projects.


Proposal

Introduce a generic helper like Register[Req, Res] to simplify handler registration and OpenAPI binding.

Example usage:

Register[GetUserRequest, User](v1, "GET", "/users/{id}", GetUserHandler,
    option.Summary("Get user by ID"),
)

Generic handler for fiber example:

func GetUserHandler(c *fiber.Ctx, req *GetUserRequest) (*User, error) {
    return &User{ID: req.ID, Name: "John"}, nil
}

The helper would:

  • Register the route using fiber.Router.Add(...)
  • Auto-bind request and response types to the OpenAPI spec:
    route.With(
        option.Request(new(Req)),
        option.Response(200, new(Res)),
    )
  • Accept extra option.Option values for further customization

ExampleRegister[Req, Res] implementation for fiber:

package fiberopenapi

import (
	"github.com/gofiber/fiber/v2"
	"github.com/oaswrap/spec"
	"github.com/oaswrap/spec/option"
)

// Generic handler function signature
type Handler[Req any, Res any] func(c *fiber.Ctx, input *Req) (*Res, error)

// Register binds a typed handler to a route, and registers request/response types with spec
func Register[Req any, Res any](
	r *spec.Router,
	method string,
	path string,
	handler Handler[Req, Res],
	opts ...option.Option,
) {
	// Wrap the generic handler into a standard Fiber handler
	fiberHandler := func(c *fiber.Ctx) error {
		var req Req

		// Parse path parameters
		if err := c.ParamsParser(&req); err != nil {
			return c.Status(400).JSON(fiber.Map{"error": "invalid path parameters"})
		}

		// Parse request body if any
		if err := c.BodyParser(&req); err != nil {
			// Optional: log or ignore body parse errors when not needed
		}

		res, err := handler(c, &req)
		if err != nil {
			return c.Status(500).JSON(fiber.Map{"error": err.Error()})
		}

		return c.JSON(res)
	}

	// Register with both Fiber and spec
	route := r.Add(path, method, fiberHandler)

	// Automatically bind request/response types to the spec
	route.With(
		option.Request(new(Req)),
		option.Response(200, new(Res)),
	)

	// Apply additional user-provided options
	for _, opt := range opts {
		route.With(opt)
	}
}

Benefits

  • DRY: No need to repeat types already defined in your handler
  • Cleaner handlers: Just define your logic and let registration handle the rest
  • Type-safe: Compile-time binding of input/output types
  • Flexible: Still allows .With(...) options as needed
  • Reusable: The generic handler(ie func GetUserHandler(c *fiber.Ctx, req *GetUserRequest) (*User, error)) is reusable from other handlers!

Tradeoffs

  • Requires adding a thin abstraction around route registration
  • For each supported web framework, similar Register[Req, Res] method is needed

I'd love to hear your thoughts — and I’d be happy to contribute a PR if this is something you'd like to support in oaswrap/spec.

Thanks again for your work!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions