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
“`
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
“`
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