httpsuite

package module
v3.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2026 License: MIT Imports: 10 Imported by: 0

README

httpsuite

httpsuite is a Go library for request parsing, response writing, and RFC 9457 problem responses.

v3 keeps the root module stdlib-only and moves validation to an optional submodule.

Features

  • Parse JSON request bodies with a default 1 MiB limit
  • Return 413 Payload Too Large when the configured body limit is exceeded
  • Bind path params explicitly through a router-specific extractor
  • Validate automatically during ParseRequest when a global validator is configured
  • Keep ParseRequest panic-safe for invalid inputs and return regular errors instead
  • Return consistent RFC 9457 Problem Details
  • Write success responses with optional generic metadata
  • Support both direct helpers and optional builders

Supported routers

Installation

Core:

go get github.com/rluders/httpsuite/v3

Optional validation adapter:

go get github.com/rluders/httpsuite/validation/playground

Mental model

  • request in: ParseRequest(...)
  • success out: OK(...), Created(...), Reply().Meta(...).OK(...)
  • problem out: ProblemResponse(...), NewBadRequestProblem(...), Problem(...).Build()
  • validation: configure once with SetValidator(...), override locally with ParseOptions.Validator

For simple handlers, prefer direct helpers.

When a handler needs custom headers, meta, or problem composition, use the optional builders.

ParseRequest never panics on invalid inputs such as a nil request, nil body, or nil path extractor. These cases return regular Go errors so callers can fail safely.

Quick start

Core only
package main

import (
	"net/http"
	"strconv"

	"github.com/go-chi/chi/v5"
	"github.com/rluders/httpsuite/v3"
)

type CreateUserRequest struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

func (r *CreateUserRequest) SetParam(fieldName, value string) error {
	if fieldName != "id" {
		return nil
	}

	id, err := strconv.Atoi(value)
	if err != nil {
		return err
	}
	r.ID = id
	return nil
}

func main() {
	router := chi.NewRouter()

	router.Post("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
		req, err := httpsuite.ParseRequest[*CreateUserRequest](w, r, chi.URLParam, nil, "id")
		if err != nil {
			return
		}

		httpsuite.OK(w, req)
	})

	_ = http.ListenAndServe(":8080", router)
}

Try it:

curl -X POST http://localhost:8080/users/123 \
  -H "Content-Type: application/json" \
  -d '{"name":"Ada"}'
Core + validation
validator := playground.NewWithValidator(nil, &httpsuite.ProblemConfig{
	BaseURL: "https://api.example.com",
})

httpsuite.SetValidator(validator)

// Validation uses the problem status returned by the configured validator.
// If the validator returns 422, ParseRequest writes 422.

req, err := httpsuite.ParseRequest[*CreateUserRequest](
	w,
	r,
	chi.URLParam,
	&httpsuite.ParseOptions{
		MaxBodyBytes: 1 << 20,
	},
	"id",
)
Direct helpers
httpsuite.OK(w, user)
httpsuite.OKWithMeta(w, users, httpsuite.NewPageMeta(page, pageSize, totalItems))
httpsuite.Created(w, user, "/users/42")
httpsuite.ProblemResponse(w, httpsuite.NewNotFoundProblem("user not found"))
Fluent helpers
httpsuite.Reply().
	Meta(httpsuite.NewPageMeta(page, pageSize, totalItems)).
	OK(w, users)

httpsuite.Reply().
	Header("X-Request-ID", requestID).
	Created(w, user, "/users/42")
Builders
problem := httpsuite.Problem(http.StatusNotFound).
	Type(httpsuite.GetProblemTypeURL("not_found_error")).
	Title("User Not Found").
	Detail("user 42 does not exist").
	Instance("/users/42").
	Build()

httpsuite.RespondProblem(problem).
	Header("X-Trace-ID", traceID).
	Write(w)

Architecture

  • root module: github.com/rluders/httpsuite/v3
  • optional validation adapter: github.com/rluders/httpsuite/validation/playground
  • root stays stdlib-only
  • validation is opt-in at bootstrap, automatic at parse time when configured
  • response metadata is generic and can use PageMeta or CursorMeta
flowchart LR
    A[HTTP handler] --> B[ParseRequest]
    B --> C[Decode JSON body]
    B --> D[Bind path params]
    B --> E{validator configured?}
    E -- yes --> F[Validate request]
    E -- no --> G[typed request]
    F --> G
    G --> H[OK / Created / Reply]
    G --> I[ProblemResponse / Problem builder]
flowchart TD
    Core[httpsuite/v3 core] --> Request[request helpers]
    Core --> Response[response helpers + builders]
    Core --> Problem[problem details + config]
    Adapter[validation/playground] -->|implements Validator| Core

Migration from v2 to v3

  • update imports from github.com/rluders/httpsuite/v2 to github.com/rluders/httpsuite/v3
  • update ParseRequest calls to pass opts before pathParams
  • configure validation globally with httpsuite.SetValidator(...) or playground.RegisterDefault()
  • ParseRequest now validates automatically when a validator is configured
  • validator-provided ProblemDetails.Status is respected when valid
  • use ParseOptions.SkipValidation to opt out per call
  • use ParseOptions.Validator to override the global validator per call
  • use ProblemConfig when you want custom problem type URLs

Examples

Examples live in examples/.

examples/restapi shows:

  • global validator setup with playground
  • ProblemConfig with custom type URLs
  • create, get, and list endpoints
  • PageMeta and CursorMeta
  • direct helpers and fluent helpers together
  • custom ProblemDetails for domain-level 404s

Notes for contributors

  • request façade and helpers live in request*.go
  • response façade, helpers, builders, and write internals live in response*.go
  • problem details, config, builders, and helpers live in problem*.go

Release notes draft for v3.0.0

  • root module is now stdlib-only
  • validation moved to github.com/rluders/httpsuite/validation/playground
  • request parsing supports configurable body-size limits
  • problem type configuration is explicit via ProblemConfig
  • global validator support added via SetValidator and RegisterDefault
  • response metadata is generic, with optional PageMeta and CursorMeta

Release workflow

The release workflow supports two paths:

  • push an existing v* tag to verify and publish that release
  • run Release with workflow_dispatch and choose major, minor, or patch

On manual dispatch, the workflow finds the latest v* tag, bumps it according to the selected semantic version part, pushes the new tag, and publishes the GitHub release for that tag.

Tutorial

Contributing

Contributions are welcome:

  • open an issue
  • submit a PR
  • add a router example

License

MIT. See LICENSE.

Documentation

Index

Constants

View Source
const BlankURL = "about:blank"

Variables

This section is empty.

Functions

func BindPathParams

func BindPathParams[T any](request T, r *http.Request, paramExtractor ParamExtractor, pathParams ...string) (T, error)

BindPathParams applies extracted path params to a request object without writing HTTP responses.

func ClearValidator

func ClearValidator()

ClearValidator removes the package-level default validator.

func Created

func Created[T any](w http.ResponseWriter, data T, location string)

Created writes a 201 JSON response and optionally sets the Location header.

func DecodeRequestBody

func DecodeRequestBody[T any](r *http.Request, maxBodyBytes int64) (T, error)

DecodeRequestBody decodes a JSON request body into T without writing HTTP responses.

func GetProblemTypeURL

func GetProblemTypeURL(errorType string) string

GetProblemTypeURL returns the default problem type URL for a known error type.

func OK

func OK[T any](w http.ResponseWriter, data T)

OK writes a 200 JSON response without metadata.

func OKWithMeta

func OKWithMeta[T any](w http.ResponseWriter, data T, meta any)

OKWithMeta writes a 200 JSON response with metadata.

func ParseRequest

func ParseRequest[T any](w http.ResponseWriter, r *http.Request, paramExtractor ParamExtractor, opts *ParseOptions, pathParams ...string) (T, error)

ParseRequest parses the incoming HTTP request into a specified struct type, handling JSON decoding, request body limits, path parameter binding, and optional validation. Invalid inputs return regular errors instead of panicking.

func ProblemResponse

func ProblemResponse(w http.ResponseWriter, problem *ProblemDetails)

ProblemResponse writes a problem response using the problem's status.

func SendResponse

func SendResponse[T any](w http.ResponseWriter, code int, data T, problem *ProblemDetails, meta any)

SendResponse sends a JSON response to the client, supporting both success and error scenarios.

func SetValidator

func SetValidator(v Validator)

SetValidator configures the package-level default validator used by ParseRequest.

Types

type BodyDecodeError

type BodyDecodeError struct {
	Kind  BodyDecodeErrorKind
	Err   error
	Limit int64
}

BodyDecodeError represents a request body parsing error.

func (*BodyDecodeError) Error

func (e *BodyDecodeError) Error() string

func (*BodyDecodeError) Unwrap

func (e *BodyDecodeError) Unwrap() error

type BodyDecodeErrorKind

type BodyDecodeErrorKind string

BodyDecodeErrorKind identifies the decode failure category.

const (
	BodyDecodeErrorInvalidJSON       BodyDecodeErrorKind = "invalid_json"
	BodyDecodeErrorBodyTooLarge      BodyDecodeErrorKind = "body_too_large"
	BodyDecodeErrorMultipleDocuments BodyDecodeErrorKind = "multiple_documents"
)

type CursorMeta

type CursorMeta struct {
	NextCursor string `json:"next_cursor,omitempty"`
	PrevCursor string `json:"prev_cursor,omitempty"`
	HasNext    bool   `json:"has_next"`
	HasPrev    bool   `json:"has_prev"`
}

CursorMeta provides cursor-based pagination details.

func NewCursorMeta

func NewCursorMeta(nextCursor, prevCursor string, hasNext, hasPrev bool) *CursorMeta

NewCursorMeta builds cursor-based metadata.

type Meta

type Meta = PageMeta

Meta is kept as a compatibility alias for page-based pagination metadata.

type PageMeta

type PageMeta struct {
	Page       int `json:"page,omitempty"`
	PageSize   int `json:"page_size,omitempty"`
	TotalPages int `json:"total_pages,omitempty"`
	TotalItems int `json:"total_items,omitempty"`
}

PageMeta provides page-based pagination details.

func NewPageMeta

func NewPageMeta(page, pageSize, totalItems int) *PageMeta

NewPageMeta builds page-based metadata and derives total pages when possible.

type ParamExtractor

type ParamExtractor func(r *http.Request, key string) string

ParamExtractor extracts a path parameter from a request.

type ParseOptions

type ParseOptions struct {
	MaxBodyBytes   int64
	Problems       *ProblemConfig
	Validator      Validator
	SkipValidation bool
}

ParseOptions configures request parsing behavior.

type PathParamError

type PathParamError struct {
	Param   string
	Missing bool
	Err     error
}

PathParamError represents a path parameter binding error.

func (*PathParamError) Error

func (e *PathParamError) Error() string

func (*PathParamError) Unwrap

func (e *PathParamError) Unwrap() error

type ProblemBuilder

type ProblemBuilder struct {
	// contains filtered or unexported fields
}

ProblemBuilder builds ProblemDetails declaratively.

func Problem

func Problem(status int) *ProblemBuilder

Problem starts a declarative ProblemDetails builder.

func ProblemBadRequest

func ProblemBadRequest(detail string) *ProblemBuilder

ProblemBadRequest returns a bad request problem builder.

func ProblemNotFound

func ProblemNotFound(detail string) *ProblemBuilder

ProblemNotFound returns a not found problem builder.

func (*ProblemBuilder) Build

func (b *ProblemBuilder) Build() *ProblemDetails

Build returns the configured ProblemDetails.

func (*ProblemBuilder) Detail

func (b *ProblemBuilder) Detail(detail string) *ProblemBuilder

Detail sets the problem detail.

func (*ProblemBuilder) Extension

func (b *ProblemBuilder) Extension(key string, value any) *ProblemBuilder

Extension sets a single problem extension.

func (*ProblemBuilder) Extensions

func (b *ProblemBuilder) Extensions(values map[string]any) *ProblemBuilder

Extensions merges multiple problem extensions.

func (*ProblemBuilder) Instance

func (b *ProblemBuilder) Instance(instance string) *ProblemBuilder

Instance sets the problem instance.

func (*ProblemBuilder) Title

func (b *ProblemBuilder) Title(title string) *ProblemBuilder

Title sets the problem title.

func (*ProblemBuilder) Type

func (b *ProblemBuilder) Type(problemType string) *ProblemBuilder

Type sets the problem type URL.

type ProblemConfig

type ProblemConfig struct {
	BaseURL        string
	ErrorTypePaths map[string]string
}

ProblemConfig controls how problem type URLs are generated.

func DefaultProblemConfig

func DefaultProblemConfig() ProblemConfig

DefaultProblemConfig returns a copy of the package default config.

func NewProblemConfig

func NewProblemConfig() ProblemConfig

NewProblemConfig returns a config preloaded with the default problem type paths.

func (ProblemConfig) Clone

func (c ProblemConfig) Clone() ProblemConfig

Clone returns a deep copy of the config.

func (ProblemConfig) TypeURL

func (c ProblemConfig) TypeURL(errorType string) string

TypeURL builds the full type URL for a known error type.

type ProblemDetails

type ProblemDetails struct {
	Type       string                 `json:"type"`
	Title      string                 `json:"title"`
	Status     int                    `json:"status"`
	Detail     string                 `json:"detail,omitempty"`
	Instance   string                 `json:"instance,omitempty"`
	Extensions map[string]interface{} `json:"extensions,omitempty"`
}

ProblemDetails conforms to RFC 9457, providing a standard format for describing errors in HTTP APIs.

func NewBadRequestProblem

func NewBadRequestProblem(detail string) *ProblemDetails

NewBadRequestProblem returns a ready-to-use bad request problem.

func NewNotFoundProblem

func NewNotFoundProblem(detail string) *ProblemDetails

NewNotFoundProblem returns a ready-to-use not found problem.

func NewProblemDetails

func NewProblemDetails(status int, problemType, title, detail string) *ProblemDetails

NewProblemDetails creates a ProblemDetails instance with standard fields.

func ValidateRequest

func ValidateRequest(request any, validator Validator) *ProblemDetails

ValidateRequest applies a validator without writing HTTP responses.

func (ProblemDetails) MarshalJSON

func (p ProblemDetails) MarshalJSON() ([]byte, error)

MarshalJSON serializes RFC 9457 extension members at the top level.

type ReplyBuilder

type ReplyBuilder struct {
	// contains filtered or unexported fields
}

ReplyBuilder configures metadata and headers before writing a response.

func Reply

func Reply() *ReplyBuilder

Reply starts a fluent response helper configuration.

func (*ReplyBuilder) Created

func (b *ReplyBuilder) Created(w http.ResponseWriter, data any, location string)

Created writes a 201 JSON response using the fluent helper configuration.

func (*ReplyBuilder) Header

func (b *ReplyBuilder) Header(key, value string) *ReplyBuilder

Header sets a single response header for a fluent helper chain.

func (*ReplyBuilder) Headers

func (b *ReplyBuilder) Headers(headers http.Header) *ReplyBuilder

Headers merges multiple response headers for a fluent helper chain.

func (*ReplyBuilder) Meta

func (b *ReplyBuilder) Meta(meta any) *ReplyBuilder

Meta sets response metadata for a fluent helper chain.

func (*ReplyBuilder) OK

func (b *ReplyBuilder) OK(w http.ResponseWriter, data any)

OK writes a 200 JSON response using the fluent helper configuration.

func (*ReplyBuilder) Problem

func (b *ReplyBuilder) Problem(w http.ResponseWriter, problem *ProblemDetails)

Problem writes a problem response using the fluent helper configuration.

type RequestParamSetter

type RequestParamSetter interface {
	SetParam(fieldName, value string) error
}

RequestParamSetter defines custom path parameter binding for request structs.

type Response

type Response[T any] struct {
	Data T   `json:"data"`
	Meta any `json:"meta,omitempty"`
}

Response represents the structure of an HTTP response, including an optional body and metadata.

type ResponseBuilder

type ResponseBuilder[T any] struct {
	// contains filtered or unexported fields
}

ResponseBuilder builds and writes HTTP responses declaratively.

func Respond

func Respond[T any](data T) *ResponseBuilder[T]

Respond starts a success response builder.

func RespondProblem

func RespondProblem(problem *ProblemDetails) *ResponseBuilder[any]

RespondProblem starts a problem response builder.

func (*ResponseBuilder[T]) Header

func (b *ResponseBuilder[T]) Header(key, value string) *ResponseBuilder[T]

Header sets a single response header.

func (*ResponseBuilder[T]) Headers

func (b *ResponseBuilder[T]) Headers(headers http.Header) *ResponseBuilder[T]

Headers merges multiple response headers.

func (*ResponseBuilder[T]) Meta

func (b *ResponseBuilder[T]) Meta(meta any) *ResponseBuilder[T]

Meta sets response metadata.

func (*ResponseBuilder[T]) Status

func (b *ResponseBuilder[T]) Status(code int) *ResponseBuilder[T]

Status overrides the response status code.

func (*ResponseBuilder[T]) Write

func (b *ResponseBuilder[T]) Write(w http.ResponseWriter)

Write writes the configured response.

type ValidationErrorDetail

type ValidationErrorDetail struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

ValidationErrorDetail provides structured details about a single validation error.

type Validator

type Validator interface {
	Validate(any) *ProblemDetails
}

Validator validates request payloads without coupling the core package to a validation library.

func DefaultValidator

func DefaultValidator() Validator

DefaultValidator returns the current package-level default validator.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL