Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


Let’s continue with our registration module, now it’s time for our service code.

Registration User Service Code

Before we move into the code, we need to be aware that as for this article we’re not yet moving our email service to another application, so we’re going to handle sending emails inside the app with goroutines.

Which means, we need to take care the panic handling of such goroutine first.

We’re going to do it with common/recover.go:

package common

import (
	"log"
	"runtime/debug"
)

func Recover() {
	if r := recover(); r != nil {
		log.Printf("panic: \n%s", string(debug.Stack()))
	}
}

And here’s finally our service/registrationdomainservice/registration_domain_service.go:

package registrationdomainservice

import (
	"context"
	"go-mini-commerce/common"
	"go-mini-commerce/entity"
	"go-mini-commerce/external/emailservice"
	"go-mini-commerce/repo/registrationdomainrepo"
	"log"
)

type (
	RegistrationDomainService interface {
		RegisterUser(
			ctx context.Context, form entity.RepoCreateAccount,
		) error
	}

	registrationDomainService struct {
		registrationDomainRepo registrationdomainrepo.RegistrationDomainRepo
		emailService           emailservice.EmailService
	}
)

func (r *registrationDomainService) RegisterUser(
	ctx context.Context, form entity.RepoCreateAccount,
) error {
	confirmationHash, err := r.registrationDomainRepo.RegisterUser(ctx, form)
	if err != nil {
		return err
	}

	go func() {
		defer common.Recover()
		err = r.emailService.UserRegistrationConfirmation(
			context.Background(), form.Name, form.Email, confirmationHash,
		)
		if err != nil {
			log.Println("cannot send registration confirmation email")
		}
	}()

	return nil
}
  • We use registrationdomainrepo.RegistrationDomainRepo from part 4. This is to call the RegisterUser method we made before in part 4. We get the confirmation from it.
  • Inside an async goroutine, there’s the call to UserRegistrationConfirmation service we made in previous part. We pass the confirmation hash, together with user’s email. This call via goroutine doesn’t involve context from outside, which means it’s a fire and forget, which also means, it might fail to deliver the email. User can just request for another confirmation hash that we will develop in future articles.
    • Because this method will run outside of http requests context, it can have panics that can’t be handled by chi middleware, that’s what common.Recover() is for.

Here’s service/registrationdomainservice/inject.go:

package registrationdomainservice

import (
	"go-mini-commerce/infra"
	"go-mini-commerce/repo/registrationdomainrepo"
	"sync"
)

var (
	registrationDomainServiceOnce sync.Once
	registrationDomainSvc         RegistrationDomainService
)

func InjectNewRegistrationDomainService(infra infra.Infra) RegistrationDomainService {
	registrationDomainServiceOnce.Do(func() {
		registrationDomainRepo := registrationdomainrepo.InjectNewRegistrationDomainRepo(infra)
		emailService := infra.External().EmailService()

		registrationDomainSvc = &registrationDomainService{
			registrationDomainRepo: registrationDomainRepo,
			emailService:           emailService,
		}
	})

	return registrationDomainSvc
}

Registration User Restful Code

Our Restful code for user registration will be simple. But before we move into the restful handler code, we need to make sure to create validations code for user’s input. This is very important. We’re going to handle each form (struct type and validation) in a go file. This is done because validation codes can be lengthy.

We will put user’s registration restful form here: restful/v1/registrationrestful/register_user_form.go.

We’ll be using ozzo-validator (from part 3) to help us creating the validations. ozzo-validator is incredibly easy and clear to use.

package registrationrestful

import (
	"fmt"

	validation "github.com/go-ozzo/ozzo-validation"
	"github.com/go-ozzo/ozzo-validation/is"
)

type (
	HttpRegisterUserForm struct {
		Email                string `json:"email"`
		Name                 string `json:"name"`
		Password             string `json:"password"`
		PasswordConfirmation string `json:"password_confirmation"`
	}
)

func (h *HttpRegisterUserForm) Validate() error {
	err := validation.Validate(h.Email, validation.Required, is.Email)
	if err != nil {
		return fmt.Errorf("validation error: %w, %s", err, "invalid email")
	}

	err = validation.Validate(h.Name, validation.Required)
	if err != nil {
		return fmt.Errorf("validation error: %w, %s", err, "invalid name")
	}

	err = validation.Validate(h.Password, validation.Required)
	if err != nil {
		return fmt.Errorf("validation error: %w, %s", err, "invalid password")
	}

	err = validation.Validate(h.PasswordConfirmation, validation.Required)
	if err != nil {
		return fmt.Errorf("validation error: %w, %s", err, "invalid password confirmation")
	}

	if h.Password != h.PasswordConfirmation {
		return fmt.Errorf("validation error: %w, %s", err, "password confirmation doesn't match")
	}

	return nil
}

There are some validations written above, we need all fields to exist. And we need password confirmation to match with password. If any of those validations are violated, that method above will surely throw error with informational message.

For our restful handler, we want to simplify our response code by using a common convention for it. Let’s make restful/httpcommon/http.go:

package httpcommon

import (
	"fmt"
	"net/http"
)

func SetResponse(w http.ResponseWriter, status int, message string) {
	w.WriteHeader(status)
	fmt.Fprintf(w, message)
}

Nothing fancy, just a simple method to define http response status code and message.

And here’s our restful handler, restful/v1/registrationrestful/registration_restful.go:

package registrationrestful

import (
	"encoding/json"
	"errors"
	"go-mini-commerce/entity"
	"go-mini-commerce/restful/httpcommon"
	"go-mini-commerce/service/registrationdomainservice"
	"net/http"
)

type (
	RegistrationRestful interface {
		RegisterUser(w http.ResponseWriter, r *http.Request)
	}

	registrationRestful struct {
		registrationDomainService registrationdomainservice.RegistrationDomainService
	}
)

func (reg *registrationRestful) RegisterUser(w http.ResponseWriter, r *http.Request) {
	var form HttpRegisterUserForm

	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&form)
	if err != nil {
		httpcommon.SetResponse(w, http.StatusBadRequest, "bad request")
		return
	}

	err = form.Validate()
	if err != nil {
		httpcommon.SetResponse(w, http.StatusUnprocessableEntity, err.Error())
		return
	}

	err = reg.registrationDomainService.RegisterUser(r.Context(), entity.RepoCreateAccount{
		Email: form.Email, Name: form.Name, Password: form.Password,
	})
	if err != nil {
		if errors.Is(err, entity.ErrUserEmailAlreadyExist) {
			httpcommon.SetResponse(w, http.StatusConflict, "email already used")
			return
		}
		httpcommon.SetResponse(w, http.StatusInternalServerError, "unknown error")
		return
	}

	httpcommon.SetResponse(w, http.StatusCreated, "user created")
}
  • We decode the json body with json.NewDecoder which receive general io.Reader data type, r.Body qualifies for that interface. If the decoder unable to decode the request body (for example if the request body is not json), we return http.StatusBadRequest.
  • We decode the json into HttpRegisterUserForm struct we made before. We then validate it. If the validation failed, we return http.StatusUnprocessableEntity.
  • We then call the RegisterUser service code with needed arguments.
    • If there’s an error thrown by RegisterUser, we will closely check, still remember that entity.ErrUserEmailAlreadyExist from part 4? Here we check if error is such type, if yes then we’ll return http.StatusConflict (status 409). Otherwise just return status internal server error.

In order for our chi router to find this service, let’s create restful/v1/registrationrestful/inject.go:

package registrationrestful

import (
	"go-mini-commerce/infra"
	"go-mini-commerce/service/registrationdomainservice"
	"sync"
)

var (
	registrationRestfulOnce sync.Once
	registrationRestfulVar  RegistrationRestful
)

func InjectNewRegistrationRestful(infra infra.Infra) RegistrationRestful {
	registrationRestfulOnce.Do(func() {
		registrationRestfulSvc := registrationdomainservice.InjectNewRegistrationDomainService(infra)

		registrationRestfulVar = &registrationRestful{
			registrationDomainService: registrationRestfulSvc,
		}
	})

	return registrationRestfulVar
}

Refactoring the Routing Code

Right now, we’re having our routing code in main.go, let’s move this into restful/restful.go, and add routing to our user registration http handler.

package restful

import (
	"go-mini-commerce/infra"
	"go-mini-commerce/restful/v1/registrationrestful"
	"net/http"

	"github.com/go-chi/chi/middleware"
	"github.com/go-chi/chi/v5"
)

type (
	RestfulServer interface {
		Run()
	}

	restfulServer struct {
		infra infra.Infra
	}
)

func InjectNewServer(infra infra.Infra) RestfulServer {
	return &restfulServer{
		infra: infra,
	}
}

func (rs *restfulServer) Run() {
	var (
		r     = chi.NewRouter()
		infra = rs.infra

		registrationRestful = registrationrestful.InjectNewRegistrationRestful(infra)
	)

	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)

	r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("pong"))
	})

	r.Route("v1", func(v1Route chi.Router) {
		v1Route.Route("/accounts", func(accountRoute chi.Router) {
			accountRoute.Route("/registration", func(registrationRoute chi.Router) {
				registrationRoute.Post("/", registrationRestful.RegisterUser)
			})
		})
	})

	http.ListenAndServe(":4545", r)
}
  • We use some chi middlewares (those are recommended by default), one of them is middleware.Recoverer which will recover the server from panics caused by any code inside the http scope.
  • registrationRestful = registrationrestful.InjectNewRegistrationRestful(infra) is where we inject infra (together with external dependencies) to our registration restful handler.
  • Thanks to awesome chi router, We’re grouping our API server there with Route(pattern string, fn func(r chi.Router)) chi.Router.

Here’s how our main.go will look like:

package main

import (
	"go-mini-commerce/external"
	"go-mini-commerce/infra"
	"go-mini-commerce/restful"
	"os"
)

func main() {
	args := os.Args

	switch args[0] {
	case "api":
		runRESTfulAPI()
	default:
		runRESTfulAPI()
	}
}

func runRESTfulAPI() {
	infraInject := infra.NewInfra(external.InjectNewExternalService(nil, nil))

	restful.InjectNewServer(infraInject).Run()
}

Testing our API

Now we’ve done creating restful API for user registration, let’s test it now. Run the restful server:

$ go1.18 run main.go api

Open another terminal and run this to test user’s registration:

curl -v --location --request POST 'localhost:4545/v1/accounts/registration/' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "dwi@test.com",
    "name": "dwi wahyudi",
    "password": "chees3pizza",
    "password_confirmation": "chees3pizza"
}'

Here’s the output, status created (201):

Let’s check the database, and verify that, such account is created together with the account email confirmation. Here we can also verify that password is correctly hashed and confirmation hash is correctly generated.

Let’s check the mailtrap inbox, to match the confirmation hash:

Now let’s test again with the same email account, we can verify 409 status code:

Now let’s test with malformed request, and confirm 400 status code:

Now let’s test with missing field, and it should respond with 422 status code:

We’ve completed our very first module in our mini commerce app. Next article will be about confirming the confirmation hash.