In part 1 we did set up the foundation of our REST API. In this part, we will set up the Register logic for our authentication portion of the app.
Authentication is a big part of almost every app we need to build as a developer. One thing it's because is so common you can almost translate the knowledge
earn in other languages. In our case for this tutorial, we will use a simple JWT authentication with an email/password combination.
Later I maybe plan to add Google OAuth.
The first thing to do is to create the User
struct. Pretty standard stuff. An id who will be auto-increment by PostgreSQL. Some timestamps so we know when the user did get created or updated.
You can also see the JSON tag. If you look at the password
I use -
, this means we do not want the password to get return to the JSON client.
// domain/users.go
package domain
import "time"
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
After this, we will create 3 instances of error. This will make our life easier in the long run of the app.
// domain/errors.go
package domain
import "errors"
var (
ErrNoResult = errors.New("no result")
ErrUserWithEmailAlreadyExist = errors.New("user with email already exist")
ErrUserWithUsernameAlreadyExist = errors.New("user with username already exist")
)
One thing I like when I work with Go is creating the interface need to make the app work before even starting writing the logic. The interface
will be like a contract and make sure my code will follow this. Time to jump on the UserRepo
interface who will be our layer to the database
for the user stuff. I also create a Domain struct who will keep the DB instance. So we can make sure we have only one instance of this last one.
This will also make life easier and no cycle dependencies issue.
// domain/domain.go
package domain
type UserRepo interface {
GetByEmail(email string) (*User, error)
GetByUsername(username string) (*User, error)
Create(user *User) (*User, error)
}
type DB struct {
UserRepo UserRepo
}
type Domain struct {
DB DB
}
With that, we can start creating the auth domain logic. First, we create a payload struct who will capture the client data request.
After this, the Register method will do the logic for creating a user to our app. This one will also handle the error if a user exists for both the
email or username, we want those to be unique. Finally, we create a method setPassword that will be filled in later part.
// domain/auth.go
package domain
type RegisterPayload struct {
Email string `json:"email"`
Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"`
Username string `json:"username"`
}
func (d *Domain) Register(payload RegisterPayload) (*User, error) {
userExist, _ := d.DB.UserRepo.GetByEmail(payload.Email)
if userExist != nil {
return nil, ErrUserWithEmailAlreadyExist
}
userExist, _ = d.DB.UserRepo.GetByUsername(payload.Username)
if userExist != nil {
return nil, ErrUserWithUsernameAlreadyExist
}
password, err := d.setPassword(payload.Password)
if err != nil {
return nil, err
}
data := &User{
Username: payload.Username,
Email: payload.Email,
Password: *password,
}
user, err := d.DB.UserRepo.Create(data)
if err != nil {
return nil, err
}
return user, nil
}
func (d *Domain) setPassword(password string) (*string, error) {
return nil, nil
}
After this, we can add the domain to your server struct. This will make the domain available to this one inside the handlers.
// handlers/handlers.go
package handlers
import (
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"todo/domain"
)
type Server struct {
domain *domain.Domain
}
func setupMiddleware(r *chi.Mux) {
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Compress(6, "application/json"))
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(middleware.Timeout(60 * time.Second))
}
func NewServer(domain *domain.Domain) *Server {
return &Server{domain: domain}
}
func SetupRouter(domain *domain.Domain) *chi.Mux {
server := NewServer(domain)
r := chi.NewRouter()
setupMiddleware(r)
server.setupEndpoints(r)
return r
}
Now we can create our users
handlers. Think of it as a controller. I like a thin controller in other frameworks like Laravel
so here I follow
the same idea.
// handlers/users.go
package handlers
import "net/http"
func (s *Server) registerUser() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, err := s.domain.Register()
}
}
We can jump after this to our data layer. Remember in the intro I did say we will use Postgres. So we will add a UserRepo
to this Postgres package.
This one will follow the UserRepo
interface from our domain. This will be standard ORM stuff.
// postgres/user.go
package postgres
import (
"errors"
"github.com/go-pg/pg/v9"
"todo/domain"
)
type UserRepo struct {
DB *pg.DB
}
func (u *UserRepo) GetByEmail(email string) (*domain.User, error) {
user := new(domain.User)
err := u.DB.Model(user).Where("email = ?", email).First()
if err != nil {
if errors.Is(err, pg.ErrNoRows) {
return nil, domain.ErrNoResult
}
return nil, err
}
return user, nil
}
func (u *UserRepo) GetByUsername(username string) (*domain.User, error) {
user := new(domain.User)
err := u.DB.Model(user).Where("username = ?", username).First()
if err != nil {
if errors.Is(err, pg.ErrNoRows) {
return nil, domain.ErrNoResult
}
return nil, err
}
return user, nil
}
func (u *UserRepo) Create(user *domain.User) (*domain.User, error) {
_, err := u.DB.Model(user).Returning("*").Insert()
if err != nil {
return nil, err
}
return user, nil
}
func NewUserRepo(DB *pg.DB) *UserRepo {
return &UserRepo{DB: DB}
}
Time to update our main.go
with the latest change needed.
// main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
"github.com/go-pg/pg/v9"
"todo/domain"
"todo/handlers"
"todo/postgres"
)
func main() {
DB := postgres.New(&pg.Options{
User: "postgres",
Password: "postgres",
Database: "todo_dev",
})
defer DB.Close()
domainDB := domain.DB{
UserRepo: postgres.NewUserRepo(DB),
}
d := &domain.Domain{DB: domainDB}
r := handlers.SetupRouter(d)
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
err := http.ListenAndServe(fmt.Sprintf(":%s", port), r)
if err != nil {
log.Fatalf("cannot start server %v", err)
}
}
Conclusion
If you did like this tutorial don't forget to subscribe to my newsletter below. Also, the video link is at the top of the post.
If you have any question don't hesitate to ask in the comment section below.
Happy Coding :)
This is a cross-platform post from my blog. You can read the original here: https://equimper.com/blog/golang-rest-api-for-nodejs-developer-part-2
Top comments (0)