Categorygithub.com/webmafia/papi
modulepackage
0.9.0
Repository: https://github.com/webmafia/papi.git
Documentation: pkg.go.dev

# 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:

TagMeaningExample sourceExample 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):

TagInt / FloatStringSliceArray
min:"*"Minimum valueMinimum lengthMinimum length-
max:"*"Maximum valueMaximum lengthMaximum length-
enum:"*,*,*"One of specific valuesOne of specific values--
pattern:"*"-Regular expression--
default:"*"Sets default if zeroSets default if zeroSets default if zeroSets default if zero
flags:"required"Must be non-zeroMust be non-zeroMust have at least 1 non-zero itemMust 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.

# Packages

No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author

# Functions

Add an advanced route.
Expose an advanced, discouraged API.
Add a route with DELETE method.
Add a route with GET method.
Create a new API service.
POST
Add a route with POST method.
Add a route with PUT method.

# Variables

No description provided by the author
API error.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
API error.
API error.

# Structs

No description provided by the author
No description provided by the author
A streaming list response.
API options.
Route information.

# Type aliases

Holds the user's request and response.