REST with Clean Architecture for Go

Build Status Coverage Status GoDevDoc Time Tracker Code lines Comments

This module implements HTTP transport level for github.com/swaggest/usecase to build REST services.

This Fork

This is a reduced fork of github.com/swaggest/rest that uses fasthttp instead of standard net/http.

fasthttp brings better performance and memory efficiency to highly loaded services, but it is not compatible with many existing middlewares and may require additional dev effort for such cases.

Goals

  • Maintain single source of truth for documentation, validation and input/output of HTTP API.
  • Avoid dependency on compile time code generation.
  • Improve productivity and reliability by abstracting HTTP details with simple API for common case.
  • Allow low-level customizations for advanced cases.
  • Maintain reasonable performance with low GC impact.

Non-Goals

  • Support for legacy documentation schemas like Swagger 2.0 or RAML.
  • Zero allocations.
  • Explicit support for XML in request or response bodies.

Features

Usage

Please check this tutorial for end-to-end usage example.

Request Decoder

Go struct with optional field tags defines input port. Request decoder populates field values from http.Request data before use case interactor is invoked.

// Declare input port type.
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"` // Field tags define parameter location and JSON schema constraints.

    // Field tags of unnamed fields are applied to parent schema, 
	// they are optional and can be used to disallow unknown parameters.
    // For non-body params, name tag must be provided explicitly.
    // E.g. here no unknown `query` and `cookie` parameters allowed,
    // unknown `header` params are ok.
    _ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
}

Input data can be located in:

  • path parameter in request URI, e.g. /users/{name},
  • query parameter in request URI, e.g. /users?locale=en-US,
  • formData parameter in request body with application/x-www-form-urlencoded or multipart/form-data content,
  • json parameter in request body with application/json content,
  • cookie parameter in request cookie,
  • header parameter in request header.

For more explicit separation of concerns between use case and transport it is possible to provide request mapping separately when initializing handler (please note, such mapping is not applied to json body).

// Declare input port type.
type helloInput struct {
    Locale string `default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$"`
    Name   string `minLength:"3"` // Field tags define parameter location and JSON schema constraints.
}

// Add use case handler with custom input mapping to router.
r.Method(http.MethodGet, "/hello/{name}", fhttp.NewHandler(u,
    fhttp.RequestMapping(new(struct {
       Locale string `query:"locale"`
       Name   string `path:"name"` // Field tags define parameter location and JSON schema constraints.
    })),
))

Additional field tags describe JSON schema constraints, please check documentation.

More schema customizations are possible with github.com/swaggest/jsonschema-go interfaces.

By default default tags are only contributing to documentation, if request.DecoderFactory.ApplyDefaults is set to true, fields of request structure that don’t have explicit value but have default will be populated with default value.

If input structure implements request.Loader, then LoadFromHTTPRequest(r *http.Request) error method will be invoked to populate input structure instead of automatic decoding. This allows low level control for cases that need it.

Response Encoder

Go struct with field tags defines output port. Response encoder writes data from output to http.ResponseWriter after use case interactor invocation finishes.

// Declare output port type.
type helloOutput struct {
    Now     time.Time `header:"X-Now" json:"-"`
    Message string    `json:"message"`
}

Output data can be located in:

  • json for response body with application/json content,
  • header for values in response header.

For more explicit separation of concerns between use case and transport it is possible to provide response header mapping separately when initializing handler.

// Declare output port type.
type helloOutput struct {
    Now     time.Time `json:"-"`
    Message string    `json:"message"`
}

// Add use case handler with custom output headers mapping to router.
r.Method(http.MethodGet, "/hello/{name}", fhttp.NewHandler(u,
    fhttp.ResponseHeaderMapping(new(struct {
        Now     time.Time `header:"X-Now"`
    })),
))

Additional field tags describe JSON schema constraints, please check documentation.

Creating Use Case Interactor

HTTP transport is decoupled from business logic by adapting use case interactors.

Use case interactor can define input and output ports that are used to map data between Go values and transport. It can provide information about itself that will be exposed in generated documentation.

// Create use case interactor with references to input/output types and interaction function.
u := usecase.NewIOI(new(helloInput), new(helloOutput), func(ctx context.Context, input, output interface{}) error {
    var (
        in  = input.(*helloInput)
        out = output.(*helloOutput)
    )

    msg, available := messages[in.Locale]
    if !available {
        return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
    }

    out.Message = fmt.Sprintf(msg, in.Name)
    out.Now = time.Now()

    return nil
})

For modularity particular use case interactor instance can be assembled by embedding relevant traits in a struct, for example you can skip adding usecase.WithInput if your use case does not imply any input.

// Create use case interactor.
u := struct {
    usecase.Info
    usecase.Interactor
    usecase.WithInput
    usecase.WithOutput
}{}

// Describe use case interactor.
u.SetTitle("Greeter")
u.SetDescription("Greeter greets you.")
u.Input = new(helloInput)
u.Output = new(helloOutput)
u.Interactor = usecase.Interact(func(ctx context.Context, input, output interface{}) error {
    // Do something about input to prepare output.
    return nil
})

Initializing Web Service

Web Service is an instrumented facade in front of router, it simplifies configuration and provides more compact API to add use cases.

// Service initializes router with required middlewares.
service := web.DefaultService()

// It allows OpenAPI configuration.
service.OpenAPI.Info.Title = "Albums API"
service.OpenAPI.Info.WithDescription("This service provides API to manage albums.")
service.OpenAPI.Info.Version = "v1.0.0"

// Additional middlewares can be added.
service.Use(
    middleware.StripSlashes,
)

// Use cases can be mounted using short syntax .<Method>(...).
service.Post("/albums", postAlbums(), fhttp.SuccessStatus(http.StatusCreated))

log.Println("Starting service at http://localhost:8080")

if err := http.ListenAndServe("localhost:8080", service); err != nil {
    log.Fatal(err)
}

Usually, web.Service API is sufficient, but if it is not, router can be configured manually, please check the documentation below.

Adding use case to router

// Add use case handler to router.
r.Method(http.MethodGet, "/hello/{name}", fhttp.NewHandler(u))

API Schema Collector

OpenAPI schema should be initialized with general information about REST API.

It uses type-safe mapping for the configuration, so any IDE will help with available fields.

// Init API documentation schema.
apiSchema := &openapi.Collector{}
apiSchema.Reflector().SpecEns().Info.Title = "Basic Example"
apiSchema.Reflector().SpecEns().Info.WithDescription("This app showcases a trivial REST API.")
apiSchema.Reflector().SpecEns().Info.Version = "v1.2.3"

Router Setup

REST router is based on github.com/go-chi/chi, wrapper allows unwrapping instrumented handler in middleware.

These middlewares are required:

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

Optionally you can add more middlewares with some performance impact:

  • request.ValidatorMiddleware(validatorFactory) (request validation, recommended)
  • response.ValidatorMiddleware(validatorFactory)
  • gzip.Middleware

You can also add any other 3rd party middlewares compatible with net/http at your discretion.

// Setup request decoder and validator.
validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
decoderFactory := request.NewDecoderFactory()
decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)

// Create router.
r := chirouter.NewWrapper(fchi.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.
)

Register Swagger UI to serve documentation at /docs.

// 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"))

Security Setup

// Prepare middleware with suitable security schema.
// It will perform actual security check for every relevant request.
adminAuth := middleware.BasicAuth("Admin Access", map[string]string{"admin": "admin"})

// Prepare API schema updater middleware.
// It will annotate handler documentation with security schema.
adminSecuritySchema := nethttp.HTTPBasicSecurityMiddleware(apiSchema, "Admin", "Admin access")

// Endpoints with admin access.
r.Route("/admin", func(r fchi.Router) {
    r.Group(func(r fchi.Router) {
        r.Wrap(adminAuth, adminSecuritySchema) // Add both middlewares to routing group to enforce and document security.
        r.Method(http.MethodPut, "/hello/{name}", fhttp.NewHandler(u))
    })
})

See example.

Handler Setup

Handler is a generalized adapter for use case interactor, so usually setup is trivial.

// Add use case handler to router.
r.Method(http.MethodGet, "/hello/{name}", fhttp.NewHandler(u))

Example

For non-generic use case, see another example.

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"github.com/swaggest/fchi"
	"github.com/swaggest/rest-fasthttp/response/gzip"
	"github.com/swaggest/rest-fasthttp/web"
	swgui "github.com/swaggest/swgui/v4emb"
	"github.com/swaggest/usecase"
	"github.com/swaggest/usecase/status"
	"github.com/valyala/fasthttp"
)

func main() {
	s := web.DefaultService()

	// Init API documentation schema.
	s.OpenAPI.Info.Title = "Basic Example"
	s.OpenAPI.Info.WithDescription("This app showcases a trivial REST API.")
	s.OpenAPI.Info.Version = "v1.2.3"

	// Setup middlewares.
	s.Wrap(
		gzip.Middleware, // Response compression with support for direct gzip pass through.
	)

	// Declare input port type.
	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"` // Field tags define parameter location and JSON schema constraints.

		// Field tags of unnamed fields are applied to parent schema.
		// they are optional and can be used to disallow unknown parameters.
		// For non-body params, name tag must be provided explicitly.
		// E.g. here no unknown `query` and `cookie` parameters allowed,
		// unknown `header` params are ok.
		_ struct{} `query:"_" cookie:"_" additionalProperties:"false"`
	}

	// Declare output port type.
	type helloOutput struct {
		Now     time.Time `header:"X-Now" json:"-"`
		Message string    `json:"message"`
	}

	messages := map[string]string{
		"en-US": "Hello, %s!",
		"ru-RU": "Привет, %s!",
	}

	// Create use case interactor with references to input/output types and interaction function.
	u := usecase.NewInteractor(func(ctx context.Context, input helloInput, output *helloOutput) error {
		msg, available := messages[input.Locale]
		if !available {
			return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
		}

		output.Message = fmt.Sprintf(msg, input.Name)
		output.Now = time.Now()

		return nil
	})

	// Describe use case interactor.
	u.SetTitle("Greeter")
	u.SetDescription("Greeter greets you.")

	u.SetExpectedErrors(status.InvalidArgument)

	// Add use case handler to router.
	s.Get("/hello/{name}", u)

	// Swagger UI endpoint at /docs.
	s.Docs("/docs", swgui.New)

	// Start server.
	log.Println("http://localhost:8011/docs")
	if err := fasthttp.ListenAndServe(":8011", fchi.RequestHandler(s)); err != nil {
		log.Fatal(err)
	}
}

Documentation Page

Versioning

This project adheres to Semantic Versioning.

Before version 1.0.0, breaking changes are tagged with MINOR bump, features and fixes are tagged with PATCH bump. After version 1.0.0, breaking changes are tagged with MAJOR bump.

Breaking changes are described in UPGRADE.md.

GitHub

View Github