Sep 20, 2024

Building & Deploying The Go Backend Of A Simple To-Do App.

Building & Deploying The Go Backend Of A Simple To-Do App.

A simple To-Do application built with Next.js, Golang & PostgreSQL. A small & fun project to get started with GO and learn its ins and outs. This is the backend only.

  • Jorge Perez Avatar
    Jorge Perez
    10 min read
  • Why I'm Interested In Golang?

    I recently took a coding test for a job, and let’s just say it reminded me how fond I am of technical interviews... Spoiler: I didn’t get the job. 😕

    Anyway, one thing that stood out was that Go was one of the language options. I’ve been noticing more and more job postings, especially in Japan, listing Go as a requirement. So, I figured it was time to familiarize myself with it.

    Project Overview

    With Go popping up more and more in job listings, I figured it was time to dive in. And what better way to learn than by building something hands-on? I decided to tackle a simple Todo app, nothing too fancy, just enough to get my feet wet.

    It was my first real experience with Go, and let’s just say it was full of surprises, mostly the good kind. I thought I’d share my journey with you—steps, code snippets, and a bit of the logic behind it all.

    Live Demo

    The App is already live, but the Fly.io backend VM is set to auto-suspend (to save a bit of cash). So please give the backend VM a moment to wake up.

    https://golang-todo-app.vercel.app/

    Getting Started

    In this post, we're sticking to the backend, focusing on Go and getting everything deployed on Fly.io. In the next post, we’ll focus on the frontend, where we’ll explore UI generation using Vercel’s v0.dev.

    1. Setting Up the Go Project

    If you haven't already, download and install Go from the official website. Make sure it's properly installed by running:

    go version

    You should see something like:

    go version go1.23.1 darwin/amd64

    This is actual version I used for this project BTW.

    1. Create the Project Directory

    I created a new directory for my project and initialized a Go module.

    mkdir go-backend
    cd go-backend
    go mod init github.com/yourusername/go-backend

    This sets up a new Go module and creates a go.mod file to manage dependencies.

    Building the Backend with Go

    1. Choosing a Web Framework

    While Go's standard library is powerful, I wanted something to make routing and middleware easier. After some research, I chose Gin, a lightweight and fast web framework.

    I installed Gin using:

    go get github.com/gin-gonic/gin
    1. Setting Up the Main Application File

    I created a main.go file as the entry point of the application.

    package main
    
    import (
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        router := gin.Default()
    
        // Define routes later here
    
        router.Run()
    }

    This sets up a basic Gin router and starts the server on the default port 8080.

    1. Defining the Todo Model

    I needed a Todo model to represent the tasks.

    package main
    
    type Todo struct {
        ID        uint   `json:"id" gorm:"primaryKey"`
        Title     string `json:"title"`
        Completed bool   `json:"completed"`
    }
    1. Setting Up the Database with GORM

    I wanted to interact with a PostgreSQL database, so I used GORM, an ORM library for Go.

    Installed GORM and the PostgreSQL driver:

    go get gorm.io/gorm
    go get gorm.io/driver/postgres
    1. Connecting to the Database

    In main.go, I set up the database connection.

    package main
    
    import (
        "github.com/gin-gonic/gin"
        "gorm.io/driver/postgres"
        "gorm.io/gorm"
        "log"
        "os"
    )
    
    var db *gorm.DB
    var err error
    
    func main() {
        dsn := os.Getenv("DATABASE_URL")
        db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err != nil {
            log.Fatal("Failed to connect to database:", err)
        }
    
        db.AutoMigrate(&Todo{})
    
        router := gin.Default()
    
        // Define routes here
    
        router.Run()
    }

    Let's examine it closely:

    • os.Getenv("DATABASE_URL"): Retrieves the database URL from an environment variable.
    • gorm.Open(): Opens a connection to the database.
    • db.AutoMigrate(&Todo): Automatically migrates the Todo model to create the table if it doesn't exist.
    1. Handling Environment Variables

    To keep things secure and flexible, I used environment variables for sensitive information like the database URL. During development, I used a .env file.

    Installed the godotenv package:

    go get github.com/joho/godotenv

    Then I updated main.go to load the .env file:

    import (
        // ... other imports ...
        "github.com/joho/godotenv"
    )
    
    func main() {
        err := godotenv.Load()
        if err != nil {
            log.Println("No .env file found, using environment variables.")
        }
    
        // ... rest of the code ...
    }

    Creating the API Endpoints

    Time to set up the routes and handlers!

    1. Structuring the Routes

    I decided to organize my code by creating separate functions for each route.

    func main() {
        // ... previous code ...
    
        router := gin.Default()
    
        router.GET("/todos", GetTodos)
        router.POST("/todos", CreateTodo)
        router.PUT("/todos/:id", UpdateTodo)
        router.DELETE("/todos/:id", DeleteTodo)
    
        router.Run()
    }
    1. Implementing the Handlers

    GetTodos:

    • Retrieves all todos from the database.
    • Returns them in JSON format.
    func GetTodos(c *gin.Context) {
        var todos []Todo
        db.Find(&todos)
        c.JSON(200, todos)
    }

    CreateTodo:

    • Binds the JSON payload to a Todo struct.
    • Creates a new record in the database.
    • Returns the created todo.
    func CreateTodo(c *gin.Context) {
        var todo Todo
        if err := c.ShouldBindJSON(&todo); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        db.Create(&todo)
        c.JSON(201, todo)
    }

    UpdateTodo:

    • Fetches the todo by ID.
    • Updates it with the new data.
    • Saves the changes to the database.
    func UpdateTodo(c *gin.Context) {
        id := c.Param("id")
        var todo Todo
        if err := db.First(&todo, id).Error; err != nil {
            c.JSON(404, gin.H{"error": "Todo not found"})
            return
        }
        if err := c.ShouldBindJSON(&todo); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        db.Save(&todo)
        c.JSON(200, todo)
    }

    DeleteTodo:

    • Fetches the todo by ID.
    • Deletes it from the database.
    • Returns a success message.
    func DeleteTodo(c *gin.Context) {
        id := c.Param("id")
        var todo Todo
        if err := db.First(&todo, id).Error; err != nil {
            c.JSON(404, gin.H{"error": "Todo not found"})
            return
        }
        db.Delete(&todo)
        c.JSON(200, gin.H{"message": "Todo deleted"})
    }

    Testing the Application Locally

    Before moving on, I wanted to ensure everything worked locally.

    1. Setting Up the Database

    I used PostgreSQL for the database. If you don't have it installed, you can get it from here.

    You can create a new one locally:

    createdb go_todo_list

    Or you can also create one in Vercel. Since my frontend will be hosted there, I ended up using that for this project. After creating it, you will get:

    POSTGRES_URL="************"
    POSTGRES_PRISMA_URL="************"
    POSTGRES_URL_NO_SSL="************"
    POSTGRES_URL_NON_POOLING="************"
    POSTGRES_USER="************"
    POSTGRES_HOST="************"
    POSTGRES_PASSWORD="************"
    POSTGRES_DATABASE="************"
    1. Configuring the .env file

    With those variables, you can create a .env file in the root directory:

    DATABASE_URL=postgres://username:password@localhost:5432/go_todo_list?sslmode=disable
    

    Replace username and password with your PostgreSQL credentials.

    1. Running the Application

    Started the server:

    go run main.go

    Output:

    [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
    
    [GIN-debug] GET    /todos                   --> main.GetTodos (3 handlers)
    [GIN-debug] POST   /todos                   --> main.CreateTodo (3 handlers)
    [GIN-debug] PUT    /todos/:id               --> main.UpdateTodo (3 handlers)
    [GIN-debug] DELETE /todos/:id               --> main.DeleteTodo (3 handlers)
    [GIN-debug] Listening and serving HTTP on :8080
    1. Testing the Endpoints

    Used curl or Postman to test the endpoints.

    Example: Creating a Todo

    curl -X POST -H "Content-Type: application/json" -d '{"title":"Learn Go","completed":false}' http://localhost:8080/todos
    

    And, the response:

    {
      "id": 1,
      "title": "Learn Go",
      "completed": false
    }

    Awesome! The backend is working locally. Now It's time to move on.

    Preparing for Production Deployment

    Before deploying, I needed to make a few adjustments.

    1. Handling CORS

    Since the frontend will be hosted on Vercel, I added CORS support.

    Installed the Gin CORS middleware:

    go get github.com/gin-contrib/cors

    Updated main.go:

    import (
        // ... other imports ...
        "github.com/gin-contrib/cors"
    )
    
    func main() {
        // ... previous code ...
    
        router := gin.Default()
    
        // Configure CORS
        router.Use(cors.Default())
    
        // ... rest of the code ...
    }

    Deploying to Fly.io

    Fly.io is a cloud hosting platform designed to run full-stack applications close to users by deploying apps to global edge locations. It is particularly developer-friendly and ideal for running containerized applications, including those built with Go (Golang).

    I ended up using Fly.io because:

    • Global Edge Deployment: Run Go apps closer to users for low latency.
    • Easy Setup: Simple CLI for fast deployment.
    • Auto Scaling: Handles traffic spikes effortlessly.
    • DDoS Protection: Built-in security for production apps.
    • Free Tier: Ideal for small projects and testing. (The free tier is especially important for me since building projects while applying to jobs can quickly get expensive).

    Okay, It's time to get this app live!

    1. Installing Flyctl

    Installed the Fly.io command-line tool:

    brew install flyctl
    1. Logging In and Creating an App

    After you create your account and verify your email, you need to login in the terminal:

    flyctl auth login

    Then, I created a new app:

    flyctl launch

    During the launch process:

    • App Name: You can accept the default or provide a custom name.
    • Select Region: Choose a region close to your target users.
    • Dockerfile: Since there's no Dockerfile yet, Fly.io will prompt to create one (we'll create it manually in the next step).
    • Create fly.toml: Accept to create this configuration file.
    • Deploy Now: Choose "n" (no) for now; we'll deploy after configuring everything.

    This command creates a fly.toml file in your backend directory, which contains configuration for your Fly.io app.

    1. Create a Dockerfile for Your Go Application

    Created a Dockerfile in the root directory:

    • Multi-stage build: To keep the final image slim.
    • Uses Alpine Linux: A lightweight base image.
    # syntax=docker/dockerfile:1
    
    # Build stage
    FROM golang:1.23.1-alpine AS builder
    
    WORKDIR /app
    
    # Copy go.mod and go.sum files
    COPY go.mod go.sum ./
    
    # Download dependencies
    RUN go mod download
    
    # Copy the source code
    COPY . .
    
    # Build the Go application
    RUN go build -o app .
    
    # Final stage
    FROM alpine:latest
    
    WORKDIR /root/
    
    # Copy the built binary from the builder stage
    COPY --from=builder /app/app .
    
    # Command to run the executable
    CMD ["./app"]
    1. Setting Environment Variables

    Set the DATABASE_URL secret, this will automatically use whatever variables you have in your .env local file and use it for deployment:

    flyctl secrets set DATABASE_URL="postgres://user:pass@hostname:5432/dbname"
    1. Deploying the Application

    The deployment process built the Docker image and released it to Fly.io.

    flyctl deploy
    1. Verifying the Deployment

    Double checked the logs just in case:

    flyctl logs

    Wrapping Up

    And there you have it! I successfully built and deployed a Go backend for a Todo app. This project was an good learning experience. Here's what I took away:

    • Go's Simplicity: The language is straightforward, and the syntax is clean.
    • Gin Framework: Makes building web applications in Go a breeze.
    • GORM ORM: Simplifies database interactions.
    • Environment Management: Using environment variables and .env files keeps configurations flexible and secure.
    • Deployment: Fly.io makes deploying applications simple and efficient.

    Final Thoughts

    If you're new to Go like I am, I highly recommend diving in with a small project like this. It helps solidify the concepts and gives you hands-on experience. Plus, it's super satisfying to see your application, the thing that you created, live and running!

    Feel free to check out the GitHub repo to see the backend code.

    Stay tuned for the next blog post, where I'll walk through building the frontend with Next.js and deploying it on Vercel.

    You Might Also Like...