/babyapi

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

Primary LanguageGoApache License 2.0Apache-2.0

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
  • Storage: 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)
  • Override any of the default handlers and use babyapi.Handler shortcut to easily render errors and responses

You can also opt to just use the api.Router() function to get the API's router/handler and add to your application's existing server.

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(
            "TODOs", "/todos",
            func() *TODO { return &TODO{} },
        )
        api.RunCLI()
    }
  3. Run!

    go mod tidy
    go run main.go serve
  4. Use the built-in CLI to interact with the API:

    # Create a new TODO
    go run main.go client todos post --data '{"title": "use babyapi!"}'
    
    # Get all TODOs
    go run main.go client todos list
    
    # Get TODO by ID (use ID from previous responses)
    go run main.go client todos get 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

The babytest package provides some shortcuts and utilities for easily building table tests or simple individual tests. This allows seamlessly creating tests for an API using the convenient babytest.RequestTest struct, a function returning an *http.Request, or a slice of command-line arguments.

Check out some of the examples for examples of using the babytest package.

If your application uses api.RunCLI(), you can execute the generate-test command to generate a boilerplate CRUD test for the API:

go run main.go generate-test

Storage

You can bring any storage backend to babyapi by implementing the Storage interface. By default, the API will use the built-in KVStorage with the default configuration for in-memory map.

This storage implementation leverages tarmac-project/hord to support a variety of key-value store backends. Currently, the babyapi/storage/kv package provides helpers to create file or redis-based storage implementations.

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

api.SetStorage(babyapi.NewKVStorage[*TODO](db, "TODO"))

EndDateable

The babyapi.EndDateable interface can be implemented to enable soft-delete with the KVStorage. This will set an end-date instead of permanently deleting a resource. Then, deleting it again will permanently delete. Also, the GetAll implementation will filter out end-dated resources unless the end_dated query parameter is set to enable getting end-dated resources.

Extensions

babyapi provides an Extension interface that can be applied to any API with api.ApplyExtension(). Implementations of this interface create custom configurations and modifications that can be applied to multiple APIs. A few extensions are provided by the babyapi/extensions package:

  • HATEOAS: "Hypertext as the engine of application state" is the 3rd and final level of REST API maturity, making your API fully RESTful
  • KVStorage: provide a few simple configurations to use the KVStorage client with a local file or Redis
  • HTMX: HTMX expects 200 responses from DELETE requests, so this changes the response code

When to use babyapi

Like anything in software engineering, there are cases where babyapi is a good choice and others where it's not a great fit.

You should use babyapi if you:

  • Need a resource-driven REST HTTP API and want to get moving quickly. This can be a standalone application or a component of a larger application
  • Have multiple APIs that will interact and can benefit from a compatible client with no extra work
  • Want to learn about framework development and contribute to an open source project
  • Know the extent of the application's scope and know it won't grow beyond babyapi's capabilities

You should not use babyapi if you:

  • Need to have ultimate control over the application's execution that might not be compatible with babyapi (although maybe you can add support!)
  • Aren't willing to dig into the framework's code and learn how it works
  • Don't understand how to build an API without it. It's important to understand the fundamentals before taking shortcuts

If babyapi is not a great fit for your use-case, you can still use some of its features to speed up development! Check out the Use As Library example.

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
  • HATEOAS Extension for hypermedia linking
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
TODO list with HTMX UI This is a more complex example that demonstrates an application with HTMX frontend. It uses server-sent events to automatically update with newly-created items
  • Implement babyapi.HTMLer for HTML responses
  • Set custom HTTP response codes per HTTP method
  • Use built-in helpers for handling server-sent events on a custom route
  • Use SetOnCreateOrUpdate to do additional actions on create
  • Handle HTML forms as input instead of JSON (which works automatically and required no changes)
Event RSVP This is a more complex nested example that implements basic authentication, middlewares, and relationships between nested types. The app can be used to create Events and provide guests with a link to view details and RSVP
  • Demonstrates middlewares and nested resource relationships
  • Authentication
  • Custom non-CRUD endpoints
  • More complex HTML templating
Multiple APIs This example shows how multiple top-level (or any level) sibling APIs can be served, and have CLI functionality, under one root API
  • Use NewRootAPI to create a root API
  • Add multiple children to create siblings
Background Worker This example shows how you can use babyapi in an application alongside background workers and have runtime control over all goroutines
  • Use WithContext to add a context to an API so the API will stop when the context is cancelled
  • Use api.Done() to have other goroutines stop when the API is stopped
SQL This example shows how you can build an API with a custom implementation of babyapi.Storage using sqlc
  • Implement an Extension using a custom implementation of babyapi.Storage
  • Use api.Done() to clean up DB resources
  • Extend the built-in CLI to add flags or other customizations
Pokemon Client This example shows how you can leverage the client and CLI features of babyapi to create a client for an external API
  • Add custom CLI command
  • Use just the client for an external API
Use As Library This example shows how a subset of babyapi features can be used as a library rather than a full framework
  • Use babyapi.Handler and babyapi.ReadRequestBodyAndDo to simplify HTTP handlers
  • Use the generic babyapi.MakeRequest function to make a request to the server
  • Use babyapi/html package to simplify responding with HTML templates

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.