swaggest/rest

repository·master·Indexed 19 days ago

https://github.com/swaggest/rest

A Go module implementing the HTTP transport layer for Clean Architecture. It enables the creation of RESTful services by mapping HTTP requests and responses to Go structs using field tags, providing a single source of truth for OpenAPI documentation, validation, and business logic. Features include request/response decoders, use case interactors, and a high-level web service facade with OpenAPI 3.1 support.

Tokens
9.4K
Snippets
33
Records
43
Agent score
65%

What's inside swaggest/rest

  1. How Request Decoders work

    master

    The Request Decoder maps HTTP request data into Go structs using field tags. This allows you to define your input port (the data your business logic needs) and its validation constraints in a single place.

    Supported parameter locations via tags:

    • path: URI parameters (e.g., /users/{name})
    • query: URL query parameters (e.g., ?locale=en-US)
    • formData: Request body with application/x-www-form-urlencoded or multipart/form-data
    • form: Acts as either formData or query
    • json: Request body with application/json
    • cookie: Request cookies
    • header: HTTP request headers
    • contentType: The content type of the raw request body

    To disallow unknown parameters, use an unnamed field with tags like query:"_" or cookie:"_" and set additionalProperties:"false".

    type helloInput struct {
        Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
        Name   string `path:"name" minLength:"3"` 
    
        // Disallow unknown query and cookie parameters
        _ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
    }
  2. How Response Encoders work

    master

    The Response Encoder writes data from your use case's output port to the http.ResponseWriter. You define where output data should go using field tags:

    • json: Response body with application/json content
    • header: Values in the HTTP response header
    • cookie: Cookie values. You can configure cookie attributes (e.g., httponly, secure, max-age, samesite) directly in the tag using comma separation.
    • contentType: A non-empty string value used as the response body with the specified content type.
    type helloOutput struct {
        Now     time.Time `header:"X-Now" json:"-"`
        Message string    `json:"message"`
        Sess    string    `cookie:"sess,httponly,secure,max-age=86400,samesite=lax"`
    }
  3. Optimize performance with manual request loading

    master

    By default, swaggest/rest automatically decodes and validates requests. If performance is critical, you can bypass this by implementing the LoadFromHTTPRequest method on your input type. When this method is present, the library will call it instead of performing automatic decoding and validation.

    This allows you to manually extract values from the *http.Request (e.g., headers, body, or query params) in a highly optimized way.

    func (i *myInput) LoadFromHTTPRequest(r *http.Request) (err error) {
    	i.Header = r.Header.Get("X-Header")
    	return nil
    }
  4. Initialize a Web Service

    master

    The web.Service is a high-level facade that wraps a router (built with chi) and provides a simplified API for configuring OpenAPI documentation, adding middlewares, and mounting use cases.

    // Initialize service with OpenAPI 3.1 support
    service := web.NewService(openapi31.NewReflector())
    
    // Configure OpenAPI metadata
    service.OpenAPISchema().SetTitle("Albums API")
    service.OpenAPISchema().SetDescription("This service provides API to manage albums.")
    service.OpenAPISchema().SetVersion("v1.0.0")
    
    // Add global middlewares
    service.Use(middleware.StripSlashes)
    
    // Mount use cases using short syntax
    service.Post("/albums", postAlbums(), nethttp.SuccessStatus(http.StatusCreated))
    
    // Start the server
    http.ListenAndServe("localhost:8080", service)
  5. Configure Security with Cookies

    master

    For cookie-based security, you must implement a custom middleware to extract the cookie and inject it into the request context, then use nethttp.APIKeySecurityMiddleware to document the requirement in the OpenAPI schema.

    // 1. Custom middleware to extract cookie and put it in context
    sessMW := func(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if c, err := r.Cookie("sessid"); err == nil {
                r = r.WithContext(context.WithValue(r.Context(), "sessionID", c.Value))
            }
            handler.ServeHTTP(w, r)
        })
    }
    
    // 2. Middleware to document the cookie security in OpenAPI
    sessDoc := nethttp.APIKeySecurityMiddleware(s.OpenAPICollector, "User",
        "sessid", oapi.InCookie, "Session cookie.")
    
    // 3. Apply to routes
    s.Route("/deeper-with-session", func(r chi.Router) {
        r.Group(func(r chi.Router) {
            r.Use(sessMW, sessDoc)
            r.Method(http.MethodGet, "/one", nethttp.NewHandler(dummy()))
        })
    })
  6. Configure Security with Basic Auth

    master

    To implement security, you need two parts: a middleware to perform the actual check and a middleware to annotate the OpenAPI documentation so it is visible in Swagger UI.

    1. middleware.BasicAuth: Performs the credential check.
    2. nethttp.HTTPBasicSecurityMiddleware: Updates the API schema to include the security requirement.
    // 1. The actual security checker
    adminAuth := middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"})
    
    // 2. The documentation annotator
    adminSecuritySchema := nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "Admin access")
    
    // Apply to a routing group
    r.Route("/admin", func(r chi.Router) {
        r.Group(func(r chi.Router) {
            r.Use(adminAuth, adminSecuritySchema)
            r.Method(http.MethodPut, "/hello/{name}", nethttp.NewHandler(u))
        })
    })
  7. Run the basic example

    master

    You can quickly run the basic example provided in the repository to see the swaggest/rest package in action. This command initializes a temporary module, fetches the example dependency, runs the application, and then cleans up the temporary files.

    cd /tmp;go mod init foo;go get -u github.com/swaggest/rest/_examples/basic;go run github.com/swaggest/rest/_examples/basic;rm go.mod;rm go.sum
  8. Serve Swagger UI documentation

    master

    To serve the OpenAPI documentation via a Swagger UI at a specific endpoint (e.g., /docs), you need to register the openapi.json file and mount the Swagger UI handler.

    // Swagger UI endpoint at /docs.
    r.Method(http.MethodGet, "/docs/openapi.json", apiSchema)
    r.Mount("/docs", v3cdn.NewHandler(apiSchema.Reflector().Spec.Info.Title,
        "/docs/openapi.json", "/docs"))
  9. Configure the REST Router and Middlewares

    master

    The REST router is a wrapper around github.com/go-chi/chi. To function correctly, you must configure specific middlewares for OpenAPI documentation, request decoding, and response encoding.

    Required Middlewares

    • nethttp.OpenAPIMiddleware(apiSchema)
    • request.DecoderMiddleware(decoderFactory)
    • response.EncoderMiddleware

    Recommended/Optional Middlewares

    • request.ValidatorMiddleware(validatorFactory): Performs request validation (recommended).
    • response.ValidatorMiddleware(validatorFactory)
    • gzip.Middleware: Enables response compression.

    Setup Example

    To support path parameters (e.g., in Chi), you should configure the decoderFactory with chirouter.PathToURLValues.

    // Setup request decoder and validator.
    validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
    decoderFactory := request.NewDecoderFactory()
    decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
    
    // Create router.
    r := chirouter.NewWrapper(chi.NewRouter())
    
    // Setup middlewares.
    r.Use(
        middleware.Recoverer,                          // Panic recovery.
        nethttp.OpenAPIMiddleware(apiSchema),          // Documentation collector.
        request.DecoderMiddleware(decoderFactory),     // Request decoder setup.
        request.ValidatorMiddleware(validatorFactory), // Request validator setup.
        response.EncoderMiddleware,                    // Response encoder setup.
        gzip.Middleware,                               // Response compression with support for direct gzip pass through.
    )
  10. Map responses with custom ResponseHeaderMapping

    master

    Similar to request mapping, you can decouple your use case from HTTP response headers by providing a separate mapping struct when initializing the handler. This allows the use case to return a clean object while the transport layer handles header injection.

    // The use case output type
    type helloOutput struct {
        Now     time.Time `json:"-"`
        Message string    `json:"message"`
    }
    
    // Map the 'Now' field to a specific HTTP header during handler initialization
    r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u, 
        nethttp.ResponseHeaderMapping(new(struct {
            Now     time.Time `header:"X-Now"` 
        })),
    ))
  11. Map requests to use cases with custom RequestMapping

    master

    By default, the Request Decoder uses the input port type defined in the use case. However, to decouple your business logic (use case) from the transport layer (HTTP), you can provide a separate mapping struct when initializing the handler. This allows the use case to remain agnostic of HTTP-specific locations like path or query tags.

    Note: This custom mapping is not applied to the json body.

    // The use case uses a clean input type without transport tags
    type helloInput struct {
        Locale string `default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$"`
        Name   string `minLength:"3"` 
    }
    
    // Use nethttp.RequestMapping to define how HTTP data maps to the input type
    r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u, 
        nethttp.RequestMapping(new(struct {
           Locale string `query:"locale"` 
           Name   string `path:"name"` 
        })),
    ))
  12. Implement a complete REST service with swaggest/rest

    master

    To build a REST API, you follow a pattern of initializing a web service, defining input/output port types with struct tags, creating a use case interactor, and registering handlers.

    Key steps:

    1. Initialize Service: Use web.NewService with an OpenAPI reflector.
    2. Define Ports: Use Go structs for inputs and outputs. Use struct tags like query, path, header, and json to define parameter locations and JSON schema constraints (e.g., minLength, pattern, enum).
    3. Create Interactor: Use usecase.NewInteractor to define the business logic. The interactor function receives a context.Context, the input struct, and a pointer to the output struct.
    4. Register Routes: Use methods like s.Get(path, interactor) to map routes to use cases.
    5. Documentation: Use s.Docs(path, swgui.New) to serve Swagger UI.
    package main
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"net/http"
    	"time"
    
    	"github.com/swaggest/openapi-go/openapi31"
    	"github.com/swaggest/rest/response/gzip"
    	"github.com/swaggest/rest/web"
    	swgui "github.com/swaggest/swgui/v5emb"
    	"github.com/swaggest/usecase"
    	"github.com/swaggest/usecase/status"
    )
    
    func main() {
    	s := web.NewService(openapi31.NewReflector())
    
    	s.OpenAPISchema().SetTitle("Basic Example")
    	s.OpenAPISchema().SetDescription("This app showcases a trivial REST API.")
    	s.OpenAPISchema().SetVersion("v1.2.3")
    
    	s.Wrap(gzip.Middleware)
    
    	type helloInput struct {
    		Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
    		Name   string `path:"name" minLength:"3"` 
    		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
    	}
    
    	type helloOutput struct {
    		Now     time.Time `header:"X-Now" json:"-"`
    		Message string    `json:"message"`
    	}
    
    	u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
    		output.Message = fmt.Sprintf("Hello, %s!", input.Name)
    		output.Now = time.Now()
    		return nil
    	})
    
    	s.Get("/hello/{name}", u)
    	s.Docs("/docs", swgui.New)
    
    	if err := http.ListenAndServe("localhost:8011", s); err != nil {
    		log.Fatal(err)
    	}
    }