Building a Web Application in Go with Copper

Copper is an all-inclusive Go toolbox for creating web applications with less boilerplate and high focus on developer efficiency. It relies on the Go standard library to maintain the traditional Go experience while allowing you to build frontend apps along with your backend and ship everything in a single binary.

Prerequisites

  • Go v1.16+ installed
  • Node v16+ installed
  • Experience building Golang applications

Installation

To get started with Copper, run the following command on your terminal:


go install github.com/gocopper/copper@latest

Then, ensure your Copper installation works correctly by running:


copper -h

If the command doesn’t work, add the following to ~/.zshrc or ~/.bashrc:


export PATH=$PATH:$GOPATH/bin

Restart your terminal session and try copper -h again.

Configuration

Copper allows you to configure the project template using -frontend and -storage arguments. The -frontend argument configures your frontend with the following frameworks and libraries:

  • Go Templates
  • Tailwind
  • React
  • Tailwind + React

The -storage argument configures your database stack, which is sqlite3 by default. You can set it to postgres, mysql, or skip storage entirely with none.

Building a To-Do App

We’ll be building a full-stack web application that allows us to perform CRUD operations on our SQLite database. Let’s create a project that uses the Go templates for frontend:


copper create --frontend go myapp

This command creates a basic scaffold project with the Go templates for frontend and sqlite3 for the database.

Project Scaffold

The project scaffold should look like this:


myapp/
cmd/
main.go
internal/
app/
app.go
db/
db.go
migrations/
0001_initial.sql
models/
todo.go
pkg/
app/
app.go
db/
db.go
migrations/
0001_initial.sql
models/
todo.go
go.mod
go.sum
web/
public/
index.js
style.css
src/
layouts/
main.html
pages/
todos.html
tailwind.config.js

Start the app server by running:


copper serve

Open http://localhost:5901 to see the Copper welcome page.

Updating the Layout File

Let’s update the web/src/layouts/main.html file with a script tag:

“`html





My App





“`

We’ll write some JavaScript in the static/index.js file to handle the delete request in our app later.

Creating a To-Dos Page

Our to-do app requires users to add to-dos to the database. Let’s create a new to-dos page with a simple form and a section to list all to-do items.

Navigate to the pages directory and create a file called todos.html:

“`html





To-Dos

To-Dos




“`

Styling Our Sample App

Navigate to the web/public directory and update style.css with the following styles:

“`css
body {
font-family: Arial, sans-serif;
}

todo-form {

margin-bottom: 20px;

}

todo-input {

width: 50%;
height: 30px;
padding: 10px;
font-size: 18px;

}

todo-button {

width: 100px;
height: 30px;
background-color: #4CAF50;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;

}

todo-list {

list-style: none;
padding: 0;
margin: 0;

}

todo-list li {

padding: 10px;
border-bottom: 1px solid #ccc;

}

todo-list li:last-child {

border-bottom: none;

}
“`

Implementing CRUD Operations

Let’s implement the CRUD operations for our to-do app.

Create

To create a new to-do item, we need to send a POST request to the /todos endpoint.

First, let’s create a new file called queries.go in the internal/db directory:

“`go
package db

import (
“database/sql”
“errors”
)

func (db *DB) SaveTodo(todo Todo) error {
_, err := db.Exec(“INSERT INTO todos (title) VALUES (?)”, todo.Title)
if err != nil {
return errors.New(“failed to save todo”)
}
return nil
}
“`

Next, let’s create a new handler function in the internal/app/app.go file:

go
func (a *App) HandleCreateTodo(w http.ResponseWriter, r *http.Request) {
title := r.FormValue("title")
if title == "" {
http.Error(w, "title is required", http.StatusBadRequest)
return
}
todo := Todo{Title: title}
err := a.db.SaveTodo(todo)
if err != nil {
http.Error(w, "failed to save todo", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/todos", http.StatusFound)
}

Finally, let’s update the /todos endpoint to handle the POST request:

go
func (a *App) HandleTodos(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// handle GET request
case "POST":
a.HandleCreateTodo(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}

Read

To read all to-do items, we need to send a GET request to the /todos endpoint.

First, let’s create a new file called queries.go in the internal/db directory:

“`go
package db

import (
“database/sql”
“errors”
)

func (db *DB) GetTodos() ([]Todo, error) {
rows, err := db.Query(“SELECT * FROM todos”)
if err != nil {
return nil, errors.New(“failed to get todos”)
}
defer rows.Close()
var todos []Todo
for rows.Next() {
var todo Todo
err := rows.Scan(&todo.ID, &todo.Title)
if err != nil {
return nil, errors.New(“failed to scan todo”)
}
todos = append(todos, todo)
}
return todos, nil
}
“`

Next, let’s create a new handler function in the internal/app/app.go file:

“`go
func (a *App) HandleGetTodos(w http.ResponseWriter, r *http.Request) {
todos, err := a.db.GetTodos()
if err != nil {
http.Error(w, “failed to get

Leave a Reply

Your email address will not be published. Required fields are marked *