Back

Go REST API for User Management

Nicholas Lim2024-05-06

Hey everyone! I'm excited to share my experience with building a Go REST API for user management. This project was initially developed as part of my university coursework on service-oriented architecture and microservice design, where I implemented it in Python using Flask. However, I couldn't resist the temptation to rewrite it in Go, showcasing the versatility and language-agnostic nature of RESTful APIs.

The Motivation: Diving into Go 1.22's Advanced Routing

The primary reasons for rewriting the API in Go was to explore the new advanced routing features introduced in Go 1.22's net/http package. Here's an example from my code leveraging these new features:

mux := http.NewServeMux()

mux.HandleFunc("GET /users", dao.handleGetAllUsers)
mux.HandleFunc("GET /users/{id}", dao.handleGetUser)
mux.HandleFunc("PUT /users/{id}", dao.handleUpdateUser)
mux.HandleFunc("DELETE /users/{id}", dao.handleDeleteUser)
mux.HandleFunc("POST /users", dao.handleCreateUser)

As you can see, I used the new pattern syntax to define routes for different HTTP methods and paths. The GET /users route retrieves a list of all users, while GET /users/{id} retrieves a user by their ID or email (more on that later). The PUT /users/{id} and DELETE /users/{id} routes allow updating and deleting users, respectively, while POST /users is used for creating new users.

The {id} wildcard in the pattern matches any value, allowing me to extract the user identifier from the URL path. Here's how I handled that in the handleGetUser function:

func (dao *ConnectionDAO) handleGetUser(w http.ResponseWriter, r *http.Request) {
    identifier := r.PathValue("id")

    // ... check if identifier is an integer (ID) or an email
    // and fetch the user accordingly
}

The req.PathValue("id") method retrieves the value matched by the {id} wildcard, making it easy to work with dynamic path segments.

Comparing sqlc and SQLAlchemy: Database Interactions in Go and Python

While working on the Go implementation, I had the opportunity to explore sqlc, a tool for auto-generating database code in Go. As someone who initially learned database interactions through SQLAlchemy in Python, I was both intrigued and a bit intimidated by sqlc when I started working with Go.

In Flask, SQLAlchemy provides an Object-Relational Mapping (ORM) layer, allowing you to define models as Python classes that correspond to database tables. This abstraction makes it easier to work with database data using familiar object-oriented concepts. Here's an example of a User model in Flask:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    phone_number = db.Column(db.String(20))
    user_type = db.Column(db.String(20))
    address = db.Column(db.String(200))

In contrast, sqlc takes a different approach in Go. Instead of an ORM, it generates type-safe code directly from your SQL queries, providing a more explicit and low-level interface to the database. Here's an example of how I defined a UserAccount struct and corresponding queries in my Go project:

type UserAccount struct {
    ID          int32
    Name        string
    Email       string
    PhoneNumber string
    UserType    UserType
    Address     string
}

type Queries struct {
    // ... other queries ...
    GetUserById    func(ctx context.Context, id int32) (UserAccount, error)
    GetUserByEmail func(ctx context.Context, email string) (UserAccount, error)
    // ... other queries ...
}

While SQLAlchemy's ORM approach may feel more familiar to Python developers, sqlc's code generation approach in Go has its own advantages. It eliminates the need for an abstraction layer, potentially leading to more efficient and predictable database interactions. Additionally, the generated code is type-safe, catching many errors at compile-time rather than runtime.

Both tools aim to simplify database interactions in their respective languages, but they take different philosophical approaches. As a Go developer, I found sqlc to be a powerful and idiomatic tool that aligns well with Go's philosophy of favoring simplicity and explicit control over abstractions.

Considering sqlx: A Familiar Choice for Database Interaction

It was only after getting the database interactions up and running using sqlc that I discovered sqlx, a library that extends the built-in database/sql package and provides a more straightforward approach to interacting with databases. In hindsight, sqlx might have been an easier choice to implement, especially for developers with a strong background in database management. It allows you to work with type-safe raw SQL queries, which can feel more familiar for those accustomed to writing SQL directly.

sqlx is a package that extends the built-in database/sql package in Go, providing an additional layer of convenience and functionality. One of the key advantages of sqlx is that it allows you to work with type-safe raw SQL queries, which may feel more familiar to developers who have taken courses or have experience in database management.

Here's an example of how you might define a User struct and execute a query using sqlx:

type User struct {
    ID          int64  `db:"id"`
    Name        string `db:"name"`
    Email       string `db:"email"`
    PhoneNumber string `db:"phone_number"`
    UserType    string `db:"user_type"`
    Address     string `db:"address"`
}

func GetUserById(db *sqlx.DB, id int64) (User, error) {
    var user User
    err := db.Get(&user, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return User{}, err
    }
    return user, nil
}

In this example, the User struct maps its fields to the corresponding column names in the database using the db struct tag. The GetUserById function uses the db.Get method from sqlx to execute a raw SQL query and automatically scan the result into a User struct.

As is often the case in software development, you don't always discover the perfect tool or library until after you've already invested time into an alternative solution. By the time I learned about sqlx, I had already made significant progress with sqlc, and rewriting the database code didn't seem worthwhile at that stage. That said, I can appreciate the simplicity and approachability that sqlx offers, especially for developers new to Go or those with extensive experience in database management. For future projects, I'll definitely consider sqlx as a potential option, weighing the tradeoffs between its familiarity and the powerful type-safety and code generation capabilities of sqlc.

Contrasting Go and Python: User Objects and Syntax

Coming from a Python background, working with structs instead of classes felt a bit alien at first. But as I spent more time with Go, I started to appreciate the simplicity and clarity that structs bring to data management. It was a refreshing change from the complexity of inheritance and class hierarchies. In Python, we typically work with user objects, which are instances of a class. Go, on the other hand, employs structs – a lightweight and efficient way to represent user data.

While the syntax for defining and working with structs in Go felt a bit unfamiliar at first, I quickly grew to appreciate its simplicity and clarity. The absence of classes and inheritance in Go's design philosophy initially seemed limiting, but I soon realized the power of composition and the flexibility it offers.

The Journey Continues

This project has been an incredibly rewarding experience, allowing me to explore the latest Go features, compare database interaction tools like sqlc and SQLAlchemy, and gain a deeper understanding of Go's unique syntax and approach to data management.

View the project on GitHub ↗️