babyapi

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Dec 2, 2023 License: Apache-2.0 Imports: 23 Imported by: 15

README

Baby API

GitHub go.mod Go version (subdirectory of monorepo) GitHub Workflow Status License Go Reference codecov

A Go CRUD API framework so simple a baby could use it.

babyapi is a super simple framework that automatically creates an HTTP API for create, read, update, and delete operations on a struct. Simply extend the babyapi.DefaultResource type to get started.

Implement custom request/response handling by implemented Renderer and Binder from go-chi/render. Use provided extension functions to add additional API functionality:

  • OnCreateOrUpdate: additional handling for create/update requests
  • SetStorage: set a different storage backend implementing the babyapi.Storage interface
  • AddCustomRoute: add more routes on the base API
  • Patch: add custom logic for handling PATCH requests
  • And many more! (see examples and docs)

Getting Started

  1. Create a new Go module:
    mkdir babyapi-example
    cd babyapi-example
    go mod init babyapi-example
    
  2. Write main.go to create a TODO struct and initialize babyapi.API:
    package main
    
    import (
        "github.com/calvinmclean/babyapi"
    )
    
    type TODO struct {
        babyapi.DefaultResource
    
        Title       string
        Description string
        Completed   bool
    }
    
    func main() {
        api := babyapi.NewAPI[*TODO](
            "TODOs", "/todos",
            func() *TODO { return &TODO{} },
        )
        api.RunCLI()
    }
    
  3. Run!
    go mod tidy
    go run main.go
    
  4. Use the built-in CLI to interact with the API:
    # Create a new TODO
    go run main.go post TODOs '{"title": "use babyapi!"}'
    
    # Get all TODOs
    go run main.go list TODOs
    
    # Get TODO by ID (use ID from previous responses)
    go run main.go get TODOs cljvfslo4020kglbctog
    

Simple Example

Client

In addition to providing the HTTP API backend, babyapi is also able to create a client that provides access to the base endpoints:

// Create a client from an existing API struct (mostly useful for unit testing):
client := api.Client(serverURL)

// Create a client from the Resource type:
client := babyapi.NewClient[*TODO](addr, "/todos")
// Create a new TODO item
todo, err := client.Post(context.Background(), &TODO{Title: "use babyapi!"})

// Get an existing TODO item by ID
todo, err := client.Get(context.Background(), todo.GetID())

// Get all incomplete TODO items
incompleteTODOs, err := client.GetAll(context.Background(), url.Values{
    "completed": []string{"false"},
})

// Delete a TODO item
err := client.Delete(context.Background(), todo.GetID())

The client provides methods for interacting with the base API and MakeRequest and MakeRequestWithResponse to interact with custom routes. You can replace the underlying http.Client and set a request editor function that can be used to set authorization headers for a client.

Testing

babyapi also makes it easy to unit test your APIs with functions that start an HTTP server with routes, execute the provided request, and return the httptest.ResponseRecorder.

Storage

You can bring any storage backend to babyapi by implementing the Storage interface. By default, the API will use the built-in MapStorage which just uses an in-memory map.

The babyapi/storage package provides another generic Storage implementation using madflojo/hord to support a variety of key-value store backends. babyapi/storage provides helper functions for initializing the hord client for Redis or file-based storage.

db, err := storage.NewFileDB(hashmap.Config{
    Filename: "storage.json",
})
db, err := storage.NewRedisDB(redis.Config{
    Server: "localhost:6379",
})

api.SetStorage(storage.NewClient[*TODO](db, "TODO"))

Examples

Description Features
TODO list This example expands upon the base example to create a realistic TODO list application
  • Custom PATCH logic
  • Additional request validation
  • Automatically set CreatedAt field
  • Query parameter parsing to only show completed items
Nested resources Demonstrates how to build APIs with nested/related resources. The root resource is an Artist which can have Albums and MusicVideos. Then, Albums can have Songs
  • Nested API resources
  • Custom ResponseWrapper to add fields from related resources
Storage The example shows how to use the babyapi/storage package to implement persistent storage
  • Use SetStorage to use a custom storage implementation
  • Create a hord storage client using babyapi/storage

Also see a full example of an application implementing a REST API using babyapi in my automated-garden project.

Contributing

Please open issues for bugs or feature requests and feel free to create a PR.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrMethodNotAllowedResponse = &ErrResponse{HTTPStatusCode: http.StatusMethodNotAllowed, StatusText: "Method not allowed."}
View Source
var ErrNotFound = errors.New("resource not found")
View Source
var ErrNotFoundResponse = &ErrResponse{HTTPStatusCode: http.StatusNotFound, StatusText: "Resource not found."}

Functions

func GetIDParam

func GetIDParam(r *http.Request, name string) string

GetIDParam gets resource ID from the request URL for a resource by name

func GetLoggerFromContext

func GetLoggerFromContext(ctx context.Context) *slog.Logger

GetLoggerFromContext returns the structured logger from the context. It expects to use an HTTP request context to get a logger with details from middleware

func GetResourceFromContext

func GetResourceFromContext[T Resource](ctx context.Context, key ContextKey) (T, error)

GetResourceFromContext gets the API resource from request context

func IDParamKey

func IDParamKey(name string) string

IDParamKey gets the chi URL param key used for the ID of a resource

func NewContextWithLogger

func NewContextWithLogger(ctx context.Context, logger *slog.Logger) context.Context

NewContextWithLogger stores a structured logger in the context

func Test

func Test[T Resource](t *testing.T, api *API[T], r *http.Request) *httptest.ResponseRecorder

Test is meant to be used in external tests to automatically handle setting up routes and using httptest

func TestServe

func TestServe[T Resource](t *testing.T, api *API[T]) (string, func())

Test is meant to be used in external tests to automatically handle setting up routes and using httptest

func TestWithParentRoute

func TestWithParentRoute[T, P Resource](t *testing.T, api *API[T], parent P, parentName, parentBasePath string, r *http.Request) *httptest.ResponseRecorder

Test is meant to be used in external tests of nested APIs

Types

type API

type API[T Resource] struct {
	// contains filtered or unexported fields
}

API encapsulates all handlers and other pieces of code required to run the CRUID API based on the provided Resource type

func NewAPI

func NewAPI[T Resource](name, base string, instance func() T) *API[T]

NewAPI initializes an API using the provided name, base URL path, and function to create a new instance of the resource with defaults

func (*API[T]) AddCustomIDRoute

func (a *API[T]) AddCustomIDRoute(route chi.Route)

AddCustomIDRoute appends a custom API route to the base path after the ID URL parameter: /base/{ID}/custom-route. The handler for this route can access the requested resource using GetResourceFromContext

func (*API[T]) AddCustomRoute

func (a *API[T]) AddCustomRoute(route chi.Route)

AddCustomRoute appends a custom API route to the base path: /base/custom-route

func (*API[T]) AddMiddlewares

func (a *API[T]) AddMiddlewares(m chi.Middlewares)

AddMiddlewares appends chi.Middlewares to existing middlewares

func (*API[T]) AddNestedAPI

func (a *API[T]) AddNestedAPI(childAPI relatedAPI)

AddNestedAPI adds a child API to this API and initializes the parent relationship on the child's side

func (*API[T]) AnyClient

func (a *API[T]) AnyClient(addr string) *Client[*AnyResource]

AnyClient returns a new Client based on the API's configuration. It is a shortcut for NewClient

func (*API[T]) Base

func (a *API[T]) Base() string

Base returns the API's base path

func (*API[T]) Client

func (a *API[T]) Client(addr string) *Client[T]

Client returns a new Client based on the API's configuration. It is a shortcut for NewClient

func (*API[T]) Delete

func (a *API[T]) Delete(w http.ResponseWriter, r *http.Request)

Delete is used to delete the resource at /base/{ID}

func (*API[T]) Get

func (a *API[T]) Get(w http.ResponseWriter, r *http.Request)

Get is the handler for /base/{ID} and returns a requested resource by ID

func (*API[T]) GetAll

func (a *API[T]) GetAll(w http.ResponseWriter, r *http.Request)

GetAll is the handler for /base and returns an array of resources

func (*API[T]) GetFromRequest

func (a *API[T]) GetFromRequest(r *http.Request) (T, *ErrResponse)

GetFromRequest will read the API's resource type from the request body or request context

func (*API[T]) GetIDParam

func (a *API[T]) GetIDParam(r *http.Request) string

GetIDParam gets resource ID from the request URL for this API's resource

func (*API[T]) GetParentIDParam

func (a *API[T]) GetParentIDParam(r *http.Request) string

GetParentIDParam reads the URL param from the request to get the ID of the parent resource

func (*API[T]) GetRequestBodyFromContext

func (a *API[T]) GetRequestBodyFromContext(ctx context.Context) T

GetRequestBodyFromContext gets an API resource from the request context. It can only be used in URL paths that include the resource ID

func (*API[T]) GetRequestedResource

func (a *API[T]) GetRequestedResource(r *http.Request) (T, *ErrResponse)

GetRequestedResource reads the API's resource from storage based on the ID in the request URL

func (*API[T]) GetRequestedResourceAndDo

func (a *API[T]) GetRequestedResourceAndDo(do func(*http.Request, T) (render.Renderer, *ErrResponse)) http.HandlerFunc

GetRequestedResourceAndDo is a wrapper that handles getting a resource from storage based on the ID in the request URL and rendering the response. This is useful for imlementing a CustomIDRoute

func (*API[T]) GetResourceFromContext

func (a *API[T]) GetResourceFromContext(ctx context.Context) (T, error)

GetResourceFromContext gets the API resource from request context

func (*API[T]) IDParamKey

func (a *API[T]) IDParamKey() string

IDParamKey gets the chi URL param key used for this API

func (*API[T]) Name

func (a *API[T]) Name() string

Name returns the name of the API

func (*API[T]) NewContextWithRequestBody

func (a *API[T]) NewContextWithRequestBody(ctx context.Context, item T) context.Context

NewContextWithRequestBody stores the API resource in the context

func (*API[T]) Parent

func (a *API[T]) Parent() relatedAPI

Parent returns the API's parent API

func (*API[T]) ParentContextKey

func (a *API[T]) ParentContextKey() ContextKey

ParentContextKey returns the context key for the direct parent's resource

func (*API[T]) Patch

func (a *API[T]) Patch(w http.ResponseWriter, r *http.Request)

Put is used to modify resources at /base/{ID}

func (*API[T]) Post

func (a *API[T]) Post(w http.ResponseWriter, r *http.Request)

Post is used to create new resources at /base

func (*API[T]) Put

func (a *API[T]) Put(w http.ResponseWriter, r *http.Request)

Put is used to idempotently create or modify resources at /base/{ID}

func (*API[T]) ReadRequestBodyAndDo

func (a *API[T]) ReadRequestBodyAndDo(do func(*http.Request, T) (T, *ErrResponse)) http.HandlerFunc

ReadRequestBodyAndDo is a wrapper that handles decoding the request body into the resource type and rendering a response

func (*API[T]) ResponseWrapper

func (a *API[T]) ResponseWrapper(responseWrapper func(T) render.Renderer)

ResponseWrapper sets a function that returns a new Renderer before responding with T. This is used to add more data to responses that isn't directly from storage

func (*API[T]) Route

func (a *API[T]) Route(r chi.Router)

Create API routes on the given router

func (*API[T]) Router

func (a *API[T]) Router() chi.Router

Create a new router with API routes

func (*API[T]) RunCLI

func (a *API[T]) RunCLI()

func (*API[T]) RunWithArgs

func (a *API[T]) RunWithArgs(out io.Writer, args []string, port int, address string, pretty bool) error

func (*API[T]) Serve

func (a *API[T]) Serve(port string)

Serve will serve the API on the given port

func (*API[T]) SetAfterDelete

func (a *API[T]) SetAfterDelete(after func(*http.Request) *ErrResponse)

SetAfterDelete sets a function that is executed after deleting a resource. It is useful for additional cleanup or other actions that should be done after deleting

func (*API[T]) SetBeforeDelete

func (a *API[T]) SetBeforeDelete(before func(*http.Request) *ErrResponse)

SetBeforeDelete sets a function that is executing before deleting a resource. It is useful for additional validation before completing the delete

func (*API[T]) SetGetAllFilter

func (a *API[T]) SetGetAllFilter(f func(*http.Request) FilterFunc[T])

SetGetAllFilter sets a function that can use the request context to create a filter for GetAll. Use this to introduce query parameters for filtering resources

func (*API[T]) SetOnCreateOrUpdate

func (a *API[T]) SetOnCreateOrUpdate(onCreateOrUpdate func(*http.Request, T) *ErrResponse)

SetOnCreateOrUpdate runs on POST, PATCH, and PUT requests before saving the created/updated resource. This is useful for adding more validations or performing tasks related to resources such as initializing schedules or sending events

func (*API[T]) SetStorage

func (a *API[T]) SetStorage(s Storage[T])

SetStorage sets a custom storage interface for the API

func (*API[T]) Stop

func (a *API[T]) Stop()

Stop will stop the API

type AnyResource

type AnyResource map[string]any

AnyResource is intended to create a "generic" Client

func (*AnyResource) Bind

func (dr *AnyResource) Bind(r *http.Request) error

func (AnyResource) GetID

func (ar AnyResource) GetID() string

func (*AnyResource) Render

type Client

type Client[T Resource] struct {
	// contains filtered or unexported fields
}

Client is used to interact with the provided Resource's API

func NewClient

func NewClient[T Resource](addr, base string) *Client[T]

NewClient initializes a Client for interacting with the Resource API

func NewSubClient

func NewSubClient[T, R Resource](parent *Client[T], path string) *Client[R]

NewSubClient creates a Client as a child of an existing Client. This is useful for accessing nested API resources

func (*Client[T]) Delete

func (c *Client[T]) Delete(ctx context.Context, id string, parentIDs ...string) error

Delete makes a DELETE request to delete a resource by ID

func (*Client[T]) Get

func (c *Client[T]) Get(ctx context.Context, id string, parentIDs ...string) (T, error)

Get will get a resource by ID

func (*Client[T]) GetAll

func (c *Client[T]) GetAll(ctx context.Context, query url.Values, parentIDs ...string) (*ResourceList[T], error)

GetAll gets all resources from the API

func (*Client[T]) MakeRequest

func (c *Client[T]) MakeRequest(req *http.Request, expectedStatusCode int) (*http.Response, error)

MakeRequest generically sends an HTTP request after calling the request editor and checks the response code

func (*Client[T]) MakeRequestWithResponse

func (c *Client[T]) MakeRequestWithResponse(req *http.Request, expectedStatusCode int) (T, error)

MakeRequestWithResponse calls MakeRequest and decodes the response body into the Resource type

func (*Client[T]) NewRequestWithParentIDs

func (c *Client[T]) NewRequestWithParentIDs(ctx context.Context, method string, body io.Reader, id string, parentIDs ...string) (*http.Request, error)

NewRequestWithParentIDs uses http.NewRequestWithContext to create a new request using the URL created from the provided ID and parent IDs

func (*Client[T]) Patch

func (c *Client[T]) Patch(ctx context.Context, id string, resource T, parentIDs ...string) (T, error)

Patch makes a PATCH request to modify a resource by ID

func (*Client[T]) PatchRaw

func (c *Client[T]) PatchRaw(ctx context.Context, id, body string, parentIDs ...string) (T, error)

PatchRaw makes a PATCH request to modify a resource by ID. It uses the provided string as the request body

func (*Client[T]) Post

func (c *Client[T]) Post(ctx context.Context, resource T, parentIDs ...string) (T, error)

Post makes a POST request to create a new resource

func (*Client[T]) PostRaw

func (c *Client[T]) PostRaw(ctx context.Context, body string, parentIDs ...string) (T, error)

PostRaw makes a POST request using the provided string as the body

func (*Client[T]) Put

func (c *Client[T]) Put(ctx context.Context, resource T, parentIDs ...string) error

Put makes a PUT request to create/modify a resource by ID

func (*Client[T]) PutRaw

func (c *Client[T]) PutRaw(ctx context.Context, id, body string, parentIDs ...string) error

PutRaw makes a PUT request to create/modify a resource by ID. It uses the provided string as the request body

func (*Client[T]) SetHTTPClient

func (c *Client[T]) SetHTTPClient(client *http.Client)

SetHTTPClient allows overriding the Clients HTTP client with a custom one

func (*Client[T]) SetRequestEditor

func (c *Client[T]) SetRequestEditor(requestEditor RequestEditor)

SetRequestEditor sets a request editor function that is used to modify all requests before sending. This is useful for adding custom request headers or authorization

func (*Client[T]) URL

func (c *Client[T]) URL(id string, parentIDs ...string) (string, error)

URL gets the URL based on provided ID and optional parent IDs

type ContextKey

type ContextKey string

ContextKey is used to store API resources in the request context

type DefaultResource

type DefaultResource struct {
	ID ID `json:"id"`
}

DefaultResource implements Resource and uses the provided ID type. Extending this type is the easiest way to implement a Resource based around the provided ID type

func NewDefaultResource

func NewDefaultResource() DefaultResource

NewDefaultResource creates a DefaultResource with a new random ID

func (*DefaultResource) Bind

func (dr *DefaultResource) Bind(r *http.Request) error

func (*DefaultResource) GetID

func (dr *DefaultResource) GetID() string

func (*DefaultResource) Render

type ErrResponse

type ErrResponse struct {
	Err            error `json:"-"`
	HTTPStatusCode int   `json:"-"`

	StatusText string `json:"status"`          // user-level status message
	AppCode    int64  `json:"code,omitempty"`  // application-specific error code
	ErrorText  string `json:"error,omitempty"` // application-level error message, for debugging
}

ErrResponse is an error that implements Renderer to be used in HTTP response

func ErrInvalidRequest

func ErrInvalidRequest(err error) *ErrResponse

func ErrRender

func ErrRender(err error) *ErrResponse

func InternalServerError

func InternalServerError(err error) *ErrResponse

func (*ErrResponse) Error

func (e *ErrResponse) Error() string

func (*ErrResponse) Render

func (e *ErrResponse) Render(_ http.ResponseWriter, r *http.Request) error

type FilterFunc

type FilterFunc[T any] func(T) bool

FilterFunc is used for GetAll to filter resources that are read from storage

type ID

type ID struct {
	xid.ID
}

ID is a type that can be optionally used to improve Resources and their APIs. It uses xid to create unique identifiers and implements a custom Bind method to:

  • Disallow POST requests with IDs
  • Automatically set new ID on POSTed resources
  • Enforce that ID is set
  • Do not allow changing ID with PATCH

func NewID

func NewID() ID

func (*ID) Bind

func (id *ID) Bind(r *http.Request) error

type MapStorage

type MapStorage[T Resource] map[string]T

MapStorage is the default implementation of the Storage interface that just uses a map

func (MapStorage[T]) Delete

func (m MapStorage[T]) Delete(id string) error

func (MapStorage[T]) Get

func (m MapStorage[T]) Get(id string) (T, error)

func (MapStorage[T]) GetAll

func (m MapStorage[T]) GetAll(filter FilterFunc[T]) ([]T, error)

func (MapStorage[T]) Set

func (m MapStorage[T]) Set(resource T) error

type Patcher

type Patcher[T Resource] interface {
	Patch(T) *ErrResponse
}

Patcher is used to optionally-enable PATCH endpoint. Since the library cannot generically modify resources without using reflection, implement Patch function to use the input to modify the receiver

type RequestEditor

type RequestEditor = func(*http.Request) error

RequestEditor is a function that can modify the HTTP request before sending

type Resource

type Resource interface {
	comparable
	// Renderer is used to control the output behavior when creating a response.
	// Use this for any after-request logic or response modifications
	render.Renderer

	// Binder is used to control the input behavior, after decoding the request.
	// Use it for input validation or additional modification of the resource using request headers or other params
	render.Binder

	GetID() string
}

Resource is an interface/constraint used for API resources. In order to use API, you must have types that implement this. It enables HTTP request/response handling and getting resources by ID

type ResourceList

type ResourceList[T render.Renderer] struct {
	Items []T `json:"items"`
}

ResourceList is used to automatically enable the GetAll endpoint that returns an array of Resources

func (*ResourceList[T]) Render

func (rl *ResourceList[T]) Render(w http.ResponseWriter, r *http.Request) error

type Storage

type Storage[T Resource] interface {
	// Get a single resource by ID
	Get(string) (T, error)
	// GetAll will return all resources that match the provided FilterFunc
	GetAll(FilterFunc[T]) ([]T, error)
	// Set will save the provided resource
	Set(T) error
	// Delete will delete a resource by ID
	Delete(string) error
}

Storage defines how the API will interact with a storage backend

Directories

Path Synopsis
examples
nested command
simple command
storage command
todo command
sql module

Jump to

Keyboard shortcuts

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