tinka alpha
tinkaalpha

Services and providers

This chapter turns the post store into a service the whole App shares. You will learn how to:

Overview

Routes creates the store today, so only the handlers can reach it. The next chapter adds a Command that lists the posts, and the chapter after it replaces the store in tests: both need the store without going through the routes.

Every App has a container, from the di package, that knows how to build each of the App's services and how long each one lives. A service provider is where a service joins the App: its Register hook binds the service in the container, and its other hooks, all optional, run at the App's boot, start, ready and shutdown. Code anywhere in the App then asks the container for the service by its type, and gets the same one.

Create the provider

act make:provider writes a provider into app/providers and adds it to the App:

Terminal
act make:provider posts
Output
CREATE app/providers/posts_provider.go
UPDATE start/app.go
         + app.Provide(&providers.PostsProvider{})

The generator added two lines to start/app.go: the import, and app.Provide, which hands the provider to the App before it boots:

start/app.go
// Package start assembles the App: its configuration, services,
// middleware and routes. main runs the App it builds, and tests build it
// the same way.
package start

import (
	"context"

	"github.com/mucolabs/tinka"
	"github.com/mucolabs/tinka/encryption"
	"github.com/mucolabs/tinka/log"

	"blog/app/exceptions"
	"blog/app/providers"
	"blog/config"
)

// App builds the App from its configuration.
func App(cfg config.Config) (*tinka.App, error) {
	enc, err := encryption.New(cfg.App.Key)
	if err != nil {
		return nil, err
	}
	cfg.HTTP.Logger = log.New(cfg.Logger)
	cfg.HTTP.Encrypter = enc

	app := tinka.New(cfg.HTTP)
	app.Provide(&providers.PostsProvider{})
	app.OnError(exceptions.Handler)
	Kernel(app)
	app.OnStart(func(context.Context) error { return Routes(app) })
	return app, nil
}

Bind the store

The generated provider has three empty hooks: Register, Boot and Shutdown. The store needs only Register, since memory needs no check at boot and nothing to release at shutdown.

Register binds *posts.Store with Singleton: the factory you pass runs the first time something asks the container for a *posts.Store, and every later request gets that same store. The factory's return type is the binding's key, so the compiler checks the wiring, and there's no name to misspell. Replace the file's contents:

app/providers/posts_provider.go
package providers

import (
	"github.com/mucolabs/tinka/di"

	"blog/app/posts"
)

// PostsProvider adds the posts store to the App.
type PostsProvider struct{}

// Register binds the store as a singleton: the App builds one, the first
// time something asks for it, and every handler and Command shares it.
func (p *PostsProvider) Register(c *di.Container) {
	c.Singleton(func(*di.Resolver) (*posts.Store, error) {
		return posts.NewStore(), nil
	})
}

When a database replaces the store, this provider is where it grows: Register binds the connection pool, and a Boot hook checks the database answers, so a misconfigured App stops at boot instead of failing its first request.

Resolve the store in the routes

Routes runs while the App starts, after every provider registered its bindings, so it can ask the container for the store. app.Container() returns the App's container, and Make[*posts.Store]() returns the store, building it on this first call, or an error when nothing provides it. Routes returns that error, which stops the App:

start/routes.go
	postHandlers := handlers.Posts{Store: posts.NewStore()}
	store, err := app.Container().Make[*posts.Store]()
	if err != nil {
		return err
	}
	postHandlers := handlers.Posts{Store: store}
	app.Get("/posts", postHandlers.List)

The handlers resolve nothing per request: they hold the store they were given, so a request pays nothing for the container.

Check the App

Save the files. The App behaves as before, now with its store from the container. Create a post, then list them:

Terminal
curl -X POST localhost:3000/posts -d 'title=Services+and+providers&body=The+store+moves+into+the+container.'
curl localhost:3000/posts
Output
{"id":3,"title":"Services and providers","body":"The store moves into the container."}
[{"id":1,"title":"Hello, Tinka","body":"The first post on the blog."},{"id":2,"title":"Routes and handlers","body":"How a request finds its handler."},{"id":3,"title":"Services and providers","body":"The store moves into the container."}]

act about describes the App, and now lists the provider:

Terminal
act about
Output
Environment
  Mode       development
  Debug      on
  Address    :3000
  Providers  *providers.PostsProvider

The output above leaves out the first lines, which name the versions of Tinka and Go and your platform.

Recap

PostsProvider binds the store in the App's container as a singleton, and start/app.go adds the provider with app.Provide. Routes resolves the store with Make when the App starts, and a missing provider stops the App with an error before it serves anything. Next, Commands uses the same store from the terminal.

See also: Service providers, Dependency injection, Application lifecycle, Scaffolding