papi

package module
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2025 License: MIT Imports: 30 Imported by: 0

README

papi

Papi

Performant API framework in Go.

  • Reduced boilerplate
  • Only reflection on startup - no reflection during runtime
  • Highly optimized, with almost zero allocations during runtime
  • Static typing of routes
  • Automatic generation of OpenAPI documentation
  • Automatic validation (based on OpenAPI schema rules)
  • Encourages dependency injection

WARNING: This hasn't reached version 1.0.0 and is not production ready yet.

Installation

go get github.com/webmafia/papi

Usage

See the example for a full example of how it's used.

Routing

Papi uses generics in routes to leverage static typing - this also makes it possible to generate an OpenAPI documentation automatically that is always 100% up to date with the code. As generics in Go only can be used on public types and functions, the following methods are public in the package:

papi.GET[I, O any](api *papi.API, r papi.Route[I, O]) (err error)
papi.PUT[I, O any](api *papi.API, r papi.Route[I, O]) (err error)
papi.POST[I, O any](api *papi.API, r papi.Route[I, O]) (err error)
papi.DELETE[I, O any](api *papi.API, r papi.Route[I, O]) (err error)

The I and O generic types are input (request) and output (response), respectively.

It might look strange at first, but the resulting code gets pretty neat:

type req struct {}

papi.GET(api, papi.Route[req, domain.User]{
	Path: "/users/{id}",

	// A handler always accepts a request- and response type, and returns any error occured.
	Handler: func(ctx *papi.RequestCtx, req *req, resp *domain.User) (err error) {
		resp.ID = 123
		resp.Name = "John Doe"

		return
	},
})

By passing pointers of the request and response to the handler, no allocation nor unnecessary copying is needed. The response is often domain model structs, but can be any type.

But how about the request input? In the example above it's an empty struct, but let's explore this in the next section.

Request input

The request input type can have any name, but it must always be a struct. This allows us to use struct tags for some magic:

type req struct{
	Id int `param:"id"`
}

If you look at the previous example, you'll see that the Path field contains a parameter in the format {id}. As we've tagged our Id field above with param:"id", any value passed in the path will end up here. Also, as the type of the field is an int, only integers will be accepted - this is validated automatically.

The following tags are supported in the request input:

Tag Meaning Example source Example destination
param:"*" URL parameters /users/{id} 123
query:"*" Search query parameters ?foo=bar,baz []string{"bar","baz"}
body:"json" PUT and POST bodies in JSON format {"foo":"bar"} MyStruct{ Foo: "bar" }

Note that string types are not copied, which means that any values in req must not be used outside the handler.

Validation

Sometimes the type is not enought. That's why we support OpenAPI's schema rules. Take this example:

type req struct{
	OrderBy string `query:"orderby" enum:"name,email"`
	Order   string `query:"order" enum:"asc,desc"`
}

The following validation tags are supported in the request input (as well as in any nested structs):

Tag Int / Float String Slice Array
min:"*" Minimum value Minimum length Minimum length -
max:"*" Maximum value Maximum length Maximum length -
enum:"*,*,*" One of specific values One of specific values - -
pattern:"*" - Regular expression - -
default:"*" Sets default if zero Sets default if zero Sets default if zero Sets default if zero
flags:"required" Must be non-zero Must be non-zero Must have at least 1 non-zero item Must be non-zero

Please note:

  • If slices and arrays don't support a tag, it's passed to their children.
  • Pointers to any type is only required to be non-nil when required.
Routing groups & OpenAPI operations

When creating an API you usually want to inject any dependencies, e.g. a User service for any user-related routes - or "operations" as they are called in the OpenAPI specfication. Also, each operation is required to have an API-unique identiier (Operation ID), and is usually grouped by a tag.

Papi solves all this with what we call a routing group, which basically is an arbitrary struct with methods matching the func(*papi.API) error signature:

type Users struct{}

func (r Users) GetUserByID(api *papi.API) (err error) {
	type req struct {
		Id int `param:"id"`
	}

	return papi.GET(api, papi.Route[req, User]{
		Path: "/users/{id}",

		Handler: func(ctx *papi.RequestCtx, req *req, resp *domain.User) (err error) {
			resp.ID = 123
			resp.Name = "John Doe"

			return
		},
	})
}

func main() {
	// API initialization and error handling is left out for brevity

	err := api.RegisterRoutes(Users{})
}

What happens here:

  • As GetUserByID matches the func(*papi.API) error, this will be called on registration.
  • A valid OpenAPI Operation ID will be generated from the method's name, resulting in get-users-by-id.
  • A descriptive summary of the route will also be generated from the method's name, result in Get user by ID.
  • The req type won't leak outside the route.
  • All OpenAPI operations will be assigned a tag matching the group's name, in this case Users.
  • We are able to inject any dependency into the Users struct, and use them in the routes.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound      = errors.NewFrozenError("NOT_FOUND", "The API route could not be found", 404)
	ErrInvalidParams = errors.NewFrozenError("INVALID_PARAMS", "URL params count mismatch", 500)
	ErrUnknownError  = errors.NewFrozenError("UNKNOWN_ERROR", "An unknown error occured", 500)
)

API error

View Source
var (
	ErrInvalidOpenAPI      = errs.New("there must not be any existing operations in OpenAPI documentation")
	ErrMissingOpenAPI      = errs.New("no OpenAPI documentation initialized")
	ErrMissingRoutePath    = errs.New("missing route path")
	ErrMissingRouteHandler = errs.New("missing route handler")
	ErrMissingRouteMethod  = errs.New("missing route method")
)

Functions

func AddRoute

func AddRoute[I, O any](api advancedApi, r AdvancedRoute[I, O]) (err error)

Add an advanced route. This is more low-level without any real benefits, thus discouraged.

func Advanced

func Advanced(api *API) advancedApi

Expose an advanced, discouraged API.

func DELETE

func DELETE[I, O any](api *API, r Route[I, O]) (err error)

Add a route with DELETE method. Input will be validated based on OpenAPI schema rules.

func GET

func GET[I, O any](api *API, r Route[I, O]) (err error)

Add a route with GET method. Input will be validated based on OpenAPI schema rules.

func POST

func POST[I, O any](api *API, r Route[I, O]) (err error)

Add a route with POST method. Input will be validated based on OpenAPI schema rules.

func PUT

func PUT[I, O any](api *API, r Route[I, O]) (err error)

Add a route with PUT method. Input will be validated based on OpenAPI schema rules.

Types

type API

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

func NewAPI

func NewAPI(reg *registry.Registry, opt ...Options) (api *API, err error)

Create a new API service.

func (*API) Close

func (api *API) Close(grace ...time.Duration) error

Close API for new requests, and close all current requests after specified grace period (default 3 seconds).

func (*API) HasPermission added in v0.21.0

func (api *API) HasPermission(roles []string, perm security.Permission) bool

func (*API) ListenAndServe

func (api *API) ListenAndServe(addr string) error

Listen on the provided address (e.g. `localhost:3000`).

func (*API) RegisterRoutes

func (api *API) RegisterRoutes(types ...any) (err error)

Register a group of routes. Any exported methods with a signature of `func(api *papi.API) error` will be called. These methods should call either `papi.GET`, `papi.PUT`, `papi.POST`, or `papi.DELETE`.

func (*API) RegisterType

func (api *API) RegisterType(typs ...registry.TypeRegistrar) (err error)

Register a custom type, that will override any defaults.

func (*API) WriteOpenAPI

func (api *API) WriteOpenAPI(w io.Writer) error

Write API documentation to an `io.Writer`.`

type AdvancedRoute

type AdvancedRoute[I any, O any] struct {
	OperationId string
	Method      string
	Path        string
	Summary     string
	Description string
	Tags        []openapi.Tag
	Handler     func(c *RequestCtx, in *I, out *O) error
	Deprecated  bool
}

type File added in v0.10.0

type File[T FileType] struct {
	// contains filtered or unexported fields
}

func (*File[T]) SetFilename added in v0.11.0

func (f *File[T]) SetFilename(name string)

func (File[T]) TypeDescription added in v0.10.0

func (File[T]) TypeDescription(reg *registry.Registry) registry.TypeDescription

TypeDescription implements registry.TypeDescriber.

func (*File[T]) Writer added in v0.10.0

func (f *File[T]) Writer() io.Writer

type FileType added in v0.10.0

type FileType interface {
	ContentType() string
	Binary() bool
}

type List

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

A streaming list response.

func (*List[T]) SetTotal

func (l *List[T]) SetTotal(i int)

Set the total number of items that exists (used for e.g. pagination).

func (List[T]) TypeDescription

func (List[T]) TypeDescription(reg *registry.Registry) registry.TypeDescription

TypeDescription implements registry.TypeDescriber.

func (*List[T]) Write

func (l *List[T]) Write(v *T) error

Write item to stream.

func (*List[T]) WriteAll added in v0.8.0

func (l *List[T]) WriteAll(it iter.Seq2[*T, error]) (err error)

Write item iterator to stream.

type MultipartFile added in v0.18.0

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

func (MultipartFile) TypeDescription added in v0.18.0

func (m MultipartFile) TypeDescription(reg *registry.Registry) registry.TypeDescription

TypeDescription implements registry.TypeDescriber.

func (*MultipartFile) WriteTo added in v0.18.0

func (m *MultipartFile) WriteTo(w io.Writer) (n int64, err error)

WriteTo implements io.WriterTo.

type Options

type Options struct {

	// An optional (but recommended) OpenAPI document. Provided document must be unused, a.k.a. have no registered operations,
	// and will be filled with documentation for all routes.
	OpenAPI *openapi.Document

	// Any errors occured will be passed through this callback, where it has the chance to transform the error to an
	// `errors.ErrorDocumentor` (if not already). Any error that isn't transformed will be replaced with a general error message.
	TransformError func(err error) errors.ErrorDocumentor

	// Header for Cross-Origin Resource Sharing (CORS).
	CORS string
}

API options.

type RawJSON added in v0.12.0

type RawJSON []byte

func (RawJSON) MarshalJSON added in v0.12.5

func (r RawJSON) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler.

func (RawJSON) TypeDescription added in v0.12.0

func (RawJSON) TypeDescription(reg *registry.Registry) registry.TypeDescription

TypeDescription implements registry.TypeDescriber.

func (*RawJSON) UnmarshalJSON added in v0.12.5

func (r *RawJSON) UnmarshalJSON(b []byte) error

UnmarshalJSON implements json.Unmarshaler.

type RequestCtx

type RequestCtx = fasthttp.RequestCtx

Holds the user's request and response.

type Route

type Route[I any, O any] struct {

	// Mandatory route path. Can contain `{params}`.
	Path string

	// An optional description of the route (longer than the `Summary`).
	Description string

	// Mandatory handler that will be called for the route.
	Handler func(c *RequestCtx, in *I, out *O) error

	// Whether the route is deprecated and discouraged.
	Deprecated bool
}

Route information.

Jump to

Keyboard shortcuts

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