Building a RESTful Service with GoFr and Chi
Now that we’ve explored the philosophies of GoFr and Chi, let’s see how they translate into building a simple REST API. For this example, we’ll create a books
service that allows us to perform CRUD operations (Create, Read, Update, Delete) on books stored in a PostgreSQL database. Our goal is to compare the ease of ease of use, external database support, logging, and observability features provided by these frameworks.
Please find the full code for the following example here.
GoFr Implementation
package handlers
import (
"database/sql"
"errors"
"strconv"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http"
"github.com/gofr-rest-api/models"
)
type handler struct{}
func New() handler {
return handler{}
}
// GetBookByID retrieves a book by its ID from the database
func (h handler) GetBookByID(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
var book models.Book
row := ctx.SQL.QueryRowContext(ctx, "SELECT id, title, author FROM books WHERE id = $1", id)
err := row.Scan(&book.ID, &book.Title, &book.Author)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
return nil, err
}
return book, nil
}
// GetAllBooks retrieves all books from the database
func (h handler) GetAllBooks(ctx *gofr.Context) (interface{}, error) {
var books []models.Book
rows, err := ctx.SQL.QueryContext(ctx, "SELECT id, title, author FROM books")
if err != nil {
return nil, err
}
defer rows.Close()
// Iterate over the rows and append each book to the books slice
for rows.Next() {
var book models.Book
if err := rows.Scan(&book.ID, &book.Title, &book.Author); err != nil {
return nil, err
}
books = append(books, book)
}
if err := rows.Err(); err != nil {
return nil, err
}
return books, nil
}
// AddBook adds a new book to the database
func (h handler) AddBook(ctx *gofr.Context) (interface{}, error) {
var (
book models.Book
id int
)
err := ctx.Bind(&book)
if err != nil {
return nil, err
}
// Insert the new book into the database and return the generated ID
row := ctx.SQL.QueryRowContext(ctx, "INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id",
book.Title, book.Author)
err = row.Scan(&id)
if err != nil {
return nil, err
}
book.ID = id
return book, nil
}
// UpdateBook updates an existing book in the database
func (h handler) UpdateBook(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
var book models.Book
err := ctx.Bind(&book)
if err != nil {
return nil, err
}
result, err := ctx.SQL.ExecContext(ctx, "UPDATE books SET title = $1, author = $2 WHERE id = $3",
book.Title, book.Author, id)
if err != nil {
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
// Return a 404 error if no rows were affected (i.e., book not found)
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
book.ID, _ = strconv.Atoi(id)
return book, nil
}
// DeleteBook deletes a book from the database
func (h handler) DeleteBook(ctx *gofr.Context) (interface{}, error) {
id := ctx.PathParam("id")
result, err := ctx.SQL.ExecContext(ctx, "DELETE FROM books WHERE id = $1", id)
if err != nil {
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, http.ErrorEntityNotFound{Name: "book", Value: id}
}
return nil, nil
}
Chi Implementation
package handlers
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/chi-rest-api/models"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
// New creates a new handler instance
type handler struct {
DB *gorm.DB
}
func New(db *gorm.DB) handler {
return handler{db}
}
// GetBookByID retrieves a book by its ID from the database
func (h handler) GetBookByID(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
http.Error(w, "Book not found", http.StatusNotFound)
return
}
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, book)
}
// GetAllBooks retrieves all books from the database
func (h handler) GetAllBooks(w http.ResponseWriter, r *http.Request) {
var books []models.Book
result := h.DB.Find(&books)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, books)
}
// AddBook adds a new book to the database
func (h handler) AddBook(w http.ResponseWriter, r *http.Request) {
var book models.Book
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
result := h.DB.Create(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusCreated, book)
}
// UpdateBook updates an existing book in the database
func (h handler) UpdateBook(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusNotFound)
return
}
err := json.NewDecoder(r.Body).Decode(&book)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
book.ID, _ = strconv.Atoi(id) // Ensure ID remains unchanged
result = h.DB.Save(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
respondJSON(w, http.StatusOK, book)
}
// DeleteBook deletes a book from the database
func (h handler) DeleteBook(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var book models.Book
result := h.DB.First(&book, id)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusNotFound)
return
}
result = h.DB.Delete(&book)
if result.Error != nil {
http.Error(w, result.Error.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// respondJSON writes the response as JSON with the given status code
func respondJSON(w http.ResponseWriter, status int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(payload); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Observations: Beyond the Code
We’ve seen how GoFr and Chi differ in their approaches to building our sample REST API. Now, let’s explore some key considerations that go beyond the code itself:
- Database Interaction: GoFr Provides inbuilt support for database interaction. The connection can be easily set up by providing suitable configurations, simplifying the development process. In Chi, users need to manually set up database connections and handle interactions. It lacks native support for database connectivity.
- Data Binding and HTTP Status Codes: GoFr automatically binds data using
ctx.Bind
and handles sending appropriate status codes by checking the returned error. Chi doesn’t natively support data binding or common HTTP status codes requiring developers to write additional code to manage these aspects. - Logging: GoFr offers structured logging with various log levels like
DEBUG
,ERROR
, INFO
etc. which helps in better monitoring and troubleshooting of applications. Chi does not have inbuilt support for logging, necessitating the integration of external libraries for logging purposes. - Code Clarity: Both GoFr and Chi promote clean code principles. GoFr’s context-based approach and built-in functionalities can lead to concise code. Chi’s reliance on external libraries might require additional context to understand database interactions within the handler functions.