Skip to content
paologaleotti edited this page Jul 9, 2024 · 17 revisions

What is blaze?

Blaze🔥 is a Go template that provides a starting point for new projects. It is designed to be a simple, yet powerful, foundation for building web applications and APIs.

ℹ️ This document is valid for blaze v1.3.0

Features

  • Fast: Built on top of the fast and efficient Go language.
  • Simple: Designed to be easy to understand and use.
  • Flexible: Blaze provides a set of utilities and nothing more so you can build your app the way you want.
  • Extensible: The project structure is designed to have one or multiple entrypoints, so you can easily add new microservices or APIs.
  • Universal: Blaze is 100% compatible with the Go http standard library.

Getting started

You can scaffold a new project by simply copying the repository or using it as a GitHub template.

The easiest and best way to scaffold a new project is to use the cli, simply run this command:

go run github.com/paologaleotti/blaze-cli@master

To compile all entrypoints, you can use the following command in the project directory:

make

Running on AWS Lambda

Blaze has native support for AWS Lambda, thanks to the full compatibility with the std library we can leverage the official AWS Lambda http package.

A ready to use template including AWS SAM template for deploy is aviable in the feature/serverless branch.

Blaze is very fast on serverless environment, averaging 300ms on a lambda full cold start with the example Todo API and around 1000ms in a big project using MongoDB and other AWS services.

Project structure

The project structure is designed to be simple and easy to understand. It is based on the following principles:

The project structure is as follows:

  • api/ (API specs or Proto definitions)
    • openapi.yaml
  • bin/ (Compiled artifacts)
  • cmd/ (Entry points for the application)
    • api/
      • main.go
  • internal/ (Internal business logic)
    • api/
      • handlers/ (HTTP handlers and controllers)
      • init.go (Initialization logic and dependency injection)
      • routes.go (HTTP routes and middlewares)
      • env.go (Environment variables and configuration)
  • pkg/ (Common and reusable packages)
    • httpcore/
    • util/

The best way to learn blaze is to just start using it! The starter project comes with a simple Todo API that you can use as a reference.

Blaze by example 🔥

In this section you can find a series of examples that will guide you through the main parts of a standard backend API application built with Blaze.

In blaze, the main components utility package is called httpcore.

Table of contents:

Controller

A controller is a simple struct that contains all the dependencies and methods (handlers).

// Let's create a simple controller for a Todo API, with a fake in-memory database
type ApiController struct {
	db []*models.Todo
}

// NewApiController creates a new instance of the ApiController
// NOTE: NewApiController is not a method of the ApiController struct!
func NewApiController() *ApiController {
	return &ApiController{
		db: make([]*models.Todo, 0),
	}
}

The controller is then registered in the init.go file, where all the dependencies are initialized. Here we also apply the routes to the router. (See Routing section for more details)

func InitService() http.Handler {
	util.InitLogger()
	router := httpcore.NewRouter()
	//env := api.InitEnv() // get typed environment

    // Here you will initialize all the dependencies and pass them 
    // to the NewApiController function

    // In our case, we just need to create a new instance of the ApiController
	controller := NewApiController()
	ApplyRoutes(router, controller)

	return router
}

Note that the InitService function returns a standard http.Handler. This is important because it can be used with any library or tool that supports standard Go http!

Basic Handler

A blaze handler is a simple method attached to a controller that receives an HTTP request and returns a response.

The handler can be any standard Go http handler. By default, blaze uses its own httpcore handler wrapper.

func (c *ApiController) GetTodos(w http.ResponseWriter, r *http.Request) (any, int) {
    // Here we can access the controller dependencies, like our in-memory database
    todos := c.db
    return todos, http.StatusOK
}

Request body and parameters

To handle a request body, you can use the httpcore package to decode the request body into a struct. the httpcore.DecodeBody() method will return the decoded struct or an already parsed error if the body is not valid.

func (tc *TodoController) CreateTodo(w http.ResponseWriter, r *http.Request) (any, int) {

    // Decode the request body directly into a newTodo variable
	newTodo, err := httpcore.DecodeBody[models.NewTodo](w, r)
	if err != nil {
		return httpcore.ErrBadRequest.With(err), http.StatusBadRequest
	}

	todo := &models.Todo{
		Id:        uuid.New().String(),
		Title:     newTodo.Title,
		Completed: false,
	}

	tc.db = append(tc.db, todo)

	return todo, http.StatusCreated
}

You can extract path and query parameters from the request. You can also use the httpcore package to decode query parameters.

func (tc *TodoController) GetTodo(w http.ResponseWriter, r *http.Request) (any, int) {
    // Get the `id` parameter from the URL
	id := r.PathValue("id")

	for _, todo := range tc.db {
		if todo.Id == id {
			return todo, http.StatusOK
		}
	}

	return httpcore.ErrNotFound, http.StatusNotFound
}

Error handling

Blaze uses the httpcore package to handle errors and responses. A standard ApiError struct is defined and all errors returned from a handler have to be wrapped in this struct.

You can find all default errors and relative types inside the pkg/httpcore/errors.go file.

type ApiError struct {
	Title   string `json:"title"`
	Message string `json:"message"`
	Status  int    `json:"status"`
}

Blaze gives you the freedom to define your own error types and handle them as you prefer, but default errors are provided.

// Inside a handler, we can return a default error with the relative status code
return httpcore.ErrBadRequest, http.StatusBadRequest

You can append custom errors or messages to any Blaze default error.

To append a specific Go Error to a default ApiError, you can use the .With() method:

if err != nil {
    // Return a BadRequest error with an error appended
	return httpcore.ErrBadRequest.With(err), http.StatusBadRequest
}

To append a string messsage to a default ApiError, you can use the .Msg() method:

// Return a NotFound error with a custom message appended
return httpcore.ErrNotFound.Msg("the thing was not found"), http.StatusNotFound

Routing

Blaze uses the chi router, which is a lightweight, idiomatic and composable router for building Go HTTP services. The routes are applied in the routes.go file, where you can define all the routes and relative handlers.

func ApplyRoutes(router chi.Router, controller *TodoController) {
	router.Get("/todos", httpcore.Handle(controller.GetTodos))
	router.Get("/todos/{id}", httpcore.Handle(controller.GetTodo))
	router.Post("/todos", httpcore.Handle(controller.CreateTodo))
}

As you can see, the httpcore.Handle() method is used to wrap the controller methods and provide a standard way to handle the request and response.

Any standard Go http handler can be used and directly registered in any route. The ApplyRoutes function is called in the init.go file, where all the routes are registered together with the controller dependencies.

Middleware

For middlewares, we can use pre-made chi middlewares or write our own using the http standard library.

The following is a basic middleware that does nothing.

func MyUselessMiddleware() func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Do something with the request

            // Call the next middleware or the handler
			next.ServeHTTP(w, r)
		})
	}
}

You can easily pass your dependencies to the middleware and use them inside the middleware function. To apply a middleware to the router, you can use the router.Use method inside the init.go file (so you have all your dependencies to pass).

func InitService() http.Handler {
	util.InitLogger()
	env := InitEnv()
	ctx := context.Background()

	router := httpcore.NewRouter()

    // Register the middleware, passing the dependencies you want to the function
	router.Use(MyUselessMiddleware())

	ApplyRoutes(router, controller, permissions)
	return router
}