Good structure for a web service with a datastore

I am building a web service and my db is a global variable. I also have an interface that provide a few functions for accessing my db (BoltDB). My bolt package implements that interface.

I heard it’s not a great idea to keep my db global and that I should have a struct that have two fields: my db and *bolt.AdminServic and that my handlers should be methods on that struct. I am trying to refactor it but I am having trouble doing it. I even googled for ‘golang dependency injection db’ and found a good article but they don’t have a separation of db and a service like mine so it confuses me.

Can someone take a look at my code and guide me?

var store *cayley.Handle
var adminService *bolt.AdminService
var configuration config.Configuration

func init() {
	// initialize the db
	configPath := flag.String("config", "", "Path to config.json")

	flag.Parse()

	if *configPath == "" {
		*configPath = config.GetPathOfConfig()
	}

	configuration = config.ReadConf(*configPath)
}

func main() {
	var err error
	store, err = bolt.Open(configuration.DbPath)
	if err != nil {
		log.Fatal(err)
	}

	// Create admin service
	adminService = &bolt.AdminService{Store: store}

	defer store.Close()

	r := mux.NewRouter()
	r.HandleFunc("/adminlogin", adminLogin).Methods("POST")
	h := c.Handler(r)
	log.Fatal(http.ListenAndServe(":"+configuration.WebPort, h))

}

The most common way (that I am aware of) to do this is to do something like this:

func main() {
  adminService := ... // get this somehow
  ac := AdminController{
    adminService: adminService,
  }

  r := mux.NewRouter()
	r.HandleFunc("/adminlogin", ac.adminLogin).Methods("POST")
  // ... continue as you were
}

type AdminController struct {
  // by not exporting this, it is only available to methods
  // on the AdminController
  adminService *bolt.AdminService
}

// If you put this in another package you will want to export it
func (ac AdminController) adminLogin(w http.ResponseWriter, r *http.Request) {
  // do things here using ac.adminService and whatever else
  // for the  login page
}

If you have specific questions about that let me know, but I am kinda in a rush so the best I can do is a code sample right now.

Thanks! I think I got it except for one concern - my AdminService interface is not even being used (see below).
Also, can you look at NewAdminService and let me know if it looks ok? I am instantiating my db inside it and returning a new struct.

cmd/web/server.go

type AdminController struct {
	// by not exporting this, it is only available to methods on the AdminController
	adminService *bolt.AdminService
}

// TODO: is it possible to get rid of this global?
var configuration config.Configuration

func init() {
	// initialize the db
	configPath := flag.String("config", "", "Path to config.json")

	flag.Parse()

	if *configPath == "" {
		*configPath = config.GetPathOfConfig()
	}

	configuration = config.ReadConf(*configPath)
}

func main() {
	adminService, err := bolt.NewAdminService(configuration.DbPath)
	if err != nil {
		log.Fatal(err)
	}

	ac := AdminController{
		adminService: adminService,
	}

	r := mux.NewRouter()
	r.HandleFunc("/adminlogin", ac.adminLogin).Methods("POST")

	defer adminService.Store.Close()

	h := c.Handler(r)
	log.Fatal(http.ListenAndServe(":"+configuration.WebPort, h))
}

bolt/main.go

// AdminService represents a BoltDB implementation of admin.UserService.
type AdminService struct {
	Store *cayley.Handle
}

// open a db connection and store it's handle inside AdminService instance
func NewAdminService(dbFile string) (*AdminService, error) {
	graph.InitQuadStore("bolt", dbFile, nil)

	// Open and use the database
	db, err := cayley.NewGraph("bolt", dbFile, nil)

	if err != nil {
		log.Fatalln(err)
	}

	adminService := &AdminService{Store: db}

	return adminService, nil
}

The only concern I have is this: in the root folder I have all my domain related structures that are needed both by the CLI and the Web executables. (This idea is based on the standard package layout blog post) And currently this interface is not even being used:

admin.go

type AdminService interface {
	CreateAdmin(a *Admin, password string) error
	Login(password string) (string, error)
	Authenticate(jwt string) (MyCustomClaims, error)
	AddClinic(c *Clinic, email string) error
	All() ([]Admin, error)
	AllClinics() ([]Clinic, error)
	GetClinic(clinicId string) (Clinic, error)
	AddEmployee(e *NewEmployee, clinicId string, email string) error
	AllEmployees() ([]Employee, error)
	// Admin(id int) (*Admin, error)
	// Admins() ([]*Admin, error)
	// DeleteAdmin(id int) error
}

Here my project’s folders:

.
├── admin.go
├── bolt
│   ├── add-clinic.go
│   ├── add-employee.go
│   ├── authenticate.go
│   ├── create-admin.go
│   ├── delete-clinic.go
│   ├── get-clinic.go
│   ├── list-admins.go
│   ├── list-clinics.go
│   ├── list-employees.go
│   ├── login.go
│   ├── main.go
│   └── quads.go
├── cmd
│   ├── cli
│   │   ├── add-admin.go
│   │   ├── add-clinic.go
│   │   ├── add-employee.go
│   │   ├── cli
│   │   ├── config.json.sample
│   │   ├── delete-clinic.go
│   │   ├── get-clinic.go
│   │   ├── list-admins.go
│   │   ├── list-clinics.go
│   │   ├── list-employees.go
│   │   ├── list-quads.go
│   │   ├── login-admin.go
│   │   ├── main.go
│   │   ├── README.md
│   │   └── test.sh
│   └── web
│       ├── add_clinic_test.go
│       ├── admin-login.go
│       ├── config.json.sample
│       ├── location
│       ├── login_test.go
│       ├── README.md
│       ├── server.go
│       ├── tmp
│       └── web
├── config
│   ├── getdatadir_unix.go
│   ├── getdatadir_windows.go
│   └── main.go

You would probably want to just make the AdminController have an AdminService (the interface) field instead of a bolt.AdminService. Eg:

type AdminController struct {
	adminService AdminService // this is the interface one
}

Assuming bolt.AdminService implements that interface it makes testing easier because you can instantiate a fake AdminService impl to test with. Eg:

type fakeAdminService struct {}
func(f fakeAdminService) CreateAdmin(a *models.Admin, password string) error {
    // fake this for tests so you don't have to interact with a real db
}

Then you could test your controllers with something like:

// You can test all this code without a real DB and you can fake that work as you see fit
ac := AdminController{AdminService: fakeAdminService{}}

Make sense?

Great. I think I am finally using interfaces properly!

so in the future If I decide to use a different database I can do:

adminService, err := postgres.NewAdminService(configuration.DbPath)

or

adminService, err := mock.NewAdminService(configuration.DbPath)

one more question regarding my Interface:

type AdminService interface {
	CreateAdmin(a *Admin, password string) error
	Login(password string, email string) (string, error)
	Authenticate(jwt string) (*MyCustomClaims, error)
	AddClinic(c *Clinic, email string) error
	All() ([]Admin, error)
	AllClinics() ([]Clinic, error)
	GetClinic(clinicId string) (Clinic, error)
	AddEmployee(e *NewEmployee, email string) error
	AllEmployees() ([]Employee, error)
}

Notice that I am not consistent when it comes to passing and returning structs - sometime i use * and sometime I don’t. I tried looking for an answer but got a bit confused reading some of the discussions. Any practical suggestions here?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.