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 theRegisterUser
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.
- 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
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 = ®istrationDomainService{
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 generalio.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 returnhttp.StatusBadRequest
. - We decode the
json
intoHttpRegisterUserForm
struct we made before. We then validate it. If the validation failed, we returnhttp.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 thatentity.ErrUserEmailAlreadyExist
from part 4? Here we check if error is such type, if yes then we’ll returnhttp.StatusConflict
(status 409). Otherwise just return status internal server error.
- If there’s an error thrown by
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 = ®istrationRestful{
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 injectinfra
(together withexternal
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.