Copper is an all-inclusive Go toolbox for creating internet functions with much less boilerplate and excessive deal with developer effectivity, which makes constructing internet apps in Go extra fascinating and enjoyable.
Copper nonetheless depends on the Go commonplace library to take care of the standard Go expertise whereas permitting you construct frontend apps alongside along with your backend and ship every part in a single binary. It has assist for constructing full-stack internet apps with React and Tailwind within the frontend, and it additionally helps constructing APIs that give extra flexiblity to work with different frontend frameworks like Vue and Svelte.
On this article, we shall be having a look at how we are able to construct an online utility in Go together with the gocopper framework so you possibly can see the way you’d implement it in your individual tasks.
Stipulations
To get together with this tutorial, you need to have the next:
- Go v1.16+ put in
- Node v16+ put in
- Expertise constructing Golang functions
Set up
To get began with Copper, we’ll run the next command on our terminal:
$ go set up github.com/gocopper/cli/cmd/[email protected]
Run the next command to make sure your Copper set up works accurately:
$ copper -h
If copper -h
didn’t work, it most likely signifies that $GOPATH/bin
just isn’t in your $PATH
. Add the next to ~/.zshrc
or ~/.bashrc
:
export PATH=$HOME/go/bin:$PATH
Then, restart your terminal session and take a look at copper -h
once more.
Configuration
Copper means that you can configure the mission template utilizing -frontend
and -storage
arguments.
The -frontend
argument means that you can configure your frontend with the next frontend frameworks and libraries:
The -storage
argument means that you can configure your database stack, which is sqlite3
by default. You might have choices to set it to postgres
, mysql
, or skip storage solely with none
.
What we shall be constructing: A easy to-do app
We shall be constructing a full-stack internet utility that enables us to carry out CRUD operations on our SQLite database. Principally, we shall be constructing a to-do utility. Right here is how the completed app appears:
This utility permits us to get, add, replace, and delete to-do gadgets from our database. With out additional ado, let’s get began.
Venture setup
We’ll create a mission that makes use of the Go templates for frontend utilizing the go
frontend stack as follows:
copper create -frontend=go github.com/<your-username>/todolist
With the above command, copper create a fundamental scaffold mission with the Go templates for frontend and sqlite3
for the database.
Right here is how your mission scaffold ought to look:
To start out the app server, run the next command:
$ cd todolist && copper run -watch
Open http://localhost:5901 to see the Copper welcome web page.
Updating the structure file
Let’s replace the internet/src/layouts/primary.html
file with a script tag as follows:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta identify="viewport" content material="width=device-width, initial-scale=1.0" /> <title>Copper App</title> <hyperlink rel="stylesheet" href="https://weblog.logrocket.com/static/types.css" /> </head> <physique> {{ template "content material" . }} <script src="/static/index.js"></script> </physique> </html>
We’ll write some javascript within the static/index.js
file to deal with the delete request in our app as we proceed.
Our to-do app requires customers so as to add to-dos to the database. Let’s create a brand new to-dos web page with a easy kind and a piece to checklist all to-do gadgets.
Navigate to the pages listing and create a file referred to as todos.html
with the next:
{{ outline "content material"}} <div class="gr-container"> <h2 class="heading">Todo Checklist</h2> <kind class="add-todo" motion="/todos" methodology="submit"> <enter sort="textual content" identify="todo"> <button sort="submit">Add Todo</button> </kind> <div class="todo-list"> <kind class="todo-item" motion="" methodology="submit"> <enter sort="textual content" identify="todo"> <button class="replace" sort="submit">Replace Todo</button> <button class="delete" sort="button">Take away Todo</button> </kind> </div> </div> {{ finish }}
Styling our pattern app
Navigate to internet/public
listing and replace model.css
with the next:
@import url('https://fonts.googleapis.com/css2?household=Work+Sans:ital,[email protected],700;1,400&show=swap'); html, physique { margin: 0; background-color: wheat; } #app { /* background-color: whitesmoke; */ padding: 40px 0; font-family: 'Work Sans', sans-serif; text-align: middle; peak: 100%; box-sizing: border-box; } .tagline { font-style: italic; font-weight: 400; } hr { margin: 40px auto; width: 60%; border-top: 1px stable #ddd; } .video { width: 60%; margin: 0 auto; } .todo-list { background-color: greenyellow; margin-top: 2rem; width: 60%; padding: 1.5rem; } .add-todo { width: 50%; show: flex; justify-items: middle; } .add-todo enter { width: 85%; peak: 2rem; } .add-todo button { width: 15%; /* peak: 1.5rem; */ } .todo-item { show: flex; margin-bottom: 1rem; } .todo-item enter { width: 65%; peak: 1.6rem; } .replace, .delete { width: 16%; } .heading { text-align: middle; font-size: 2.5rem; } .gr-container { padding: 1rem; show: flex; flex-direction: column; align-items: middle; }
With a view to view the to-dos web page on the browser, we’ll replace the pkg/app/router.go
file as follows:
func (ro *Router) Routes() []chttp.Route { return []chttp.Route{ { Path: "https://weblog.logrocket.com/", Strategies: []string{http.MethodGet}, Handler: ro.HandleIndexPage, }, { Path: "/todos", Strategies: []string{http.MethodGet}, Handler: ro.HandleTodosPage, }, } } func (ro *Router) HandleTodosPage(w http.ResponseWriter, r *http.Request) { ro.rw.WriteHTML(w, r, chttp.WriteHTMLParams{ PageTemplate: "todos.html", }) }
Right here, we replace our app route with the "/todos"
path and likewise create the HandleTodosPage
handler, which serves todos.html
web page when a person hit the "/todos"
route within the browser.
Restart the app server and open http://localhost:5901/todos to see the to-dos web page.
Right here is how the to-dos web page ought to look:
Implement a characteristic to create now to-do gadgets
To implement the create characteristic, we’d arrange a storage layer that may save the todo gadgets into our database.
Let’s run the next command to create the pkg/todos
package deal in addition to a pkg/todos/queries.go
that we are able to use to implement our SQL queries:
copper scaffold:pkg todos copper scaffold:queries todos
Subsequent, we’ll create the Todo
mannequin and its database migration. Open up pkg/todos/fashions.go
and outline the Todo
mannequin:
sort Todo struct { Title string Rank int64 `gorm:"-"` }
Then, open up migrations/0001_initial.sql
and outline its database schema:
-- +migrate Up CREATE TABLE todos ( identify textual content ); -- +migrate Down DROP TABLE todos;
In your terminal, run copper migrate
to create the desk.
Now that we’ve got a Todo
mannequin and a todos
desk in our database, we are able to write our database queries within the pkg/posts/queries.go
file.
Let’s add a SaveTodo
methodology that can be utilized to insert new to-dos into the todos
desk:
import ( "context" ... ) func (q *Queries) SaveTodo(ctx context.Context, todo *Todo) error { const question = ` INSERT INTO todos (identify) VALUES (?)` _, err := q.querier.Exec(ctx, question, todo.Title, ) return err }
Right here, we use an SQL question to insert a brand new to-do into the DB. The query mark is a placeholder for a price within the question. The q.querier.Exec
operate takes a context object, a question, and as many arguments you need to cross into the placeholders. Then, the values within the placeholders are inserted within the order they seem within the q.querier.Exec
operate.
Now, let’s head over to pkg/app/router.go
and use the SaveTodo
methodology to deal with kind submissions for creating new to-dos.
Create a brand new route handler, HandleSubmitPost
, to deal with submissions for creating new to-dos as follows:
func (ro *Router) HandleCreateTodos(w http.ResponseWriter, r *http.Request) { var ( todo = strings.TrimSpace(r.PostFormValue(("todo"))) ) if todo == "" { ro.rw.WriteHTMLError(w, r, cerrors.New(nil, "unable to create todos: todo can't be empty", map[string]interface{}{ "kind": r.Kind, })) return } newtodo := todos.Todo{Title: todo} err := ro.todos.SaveTodo(r.Context(), &newtodo) if err != nil { ro.logger.Error("an error occured whereas saving todo", err) ro.rw.WriteHTMLError(w, r, cerrors.New(nil, "unable to create todos", map[string]interface{}{ "kind": r.Kind, })) return } http.Redirect(w, r, "/todos", http.StatusSeeOther) }
This operate creates a brand new to-do merchandise. First. we get the to-do worth from the shape and trim any white area the person could have added. Then, we verify if the to-do is an empty string, during which case we throw an error. Subsequent, we create a brand new to-do object and name the ro.todos.SaveTodo
operate to reserve it into the database. If there was an error whereas saving the to-do, we throw an error. Lastly, we redirect to the to-dos web page.
Extra nice articles from LogRocket:
Replace the pkg/app/router.go
file with the HandleCreateTodos
as follows:
import ( "strings" "github.com/gocopper/copper/cerrors" "github.com/gocopper/copper/chttp" "github.com/gocopper/copper/clogger" "internet/http" "github.com/emmanuelhashy/todolist/pkg/todos" ) sort NewRouterParams struct { Todos *todos.Queries RW *chttp.ReaderWriter Logger clogger.Logger } func NewRouter(p NewRouterParams) *Router { return &Router{ rw: p.RW, logger: p.Logger, todos: p.Todos, } } sort Router struct { rw *chttp.ReaderWriter logger clogger.Logger todos *todos.Queries } func (ro *Router) Routes() []chttp.Route { return []chttp.Route{ { Path: "https://weblog.logrocket.com/", Strategies: []string{http.MethodGet}, Handler: ro.HandleIndexPage, }, { Path: "/todos", Strategies: []string{http.MethodGet}, Handler: ro.HandleTodosPage, }, { Path: "/todos", Strategies: []string{http.MethodPost}, Handler: ro.HandleCreateTodos, }, } }
Now, the create characteristic for our todo app ought to work! We will add a brand new to-do into the todos
desk.
Add a learn characteristic to see all of the to-do gadgets
To implement the learn characteristic, we’d create a brand new SQL question to return a listing of all todos that exist within the database and work our manner up from there.
Replace pkg/todos/queries.go
with the next methodology to checklist all to-dos:
func (q *Queries) ListTodos(ctx context.Context) ([]Todo, error) { const question = "SELECT * FROM todos" var ( todos []Todo err = q.querier.Choose(ctx, &todos, question) ) return todos, err }
Right here, we use an SQL question to get all of the to-dos saved within the database. Since we aren’t updating a price within the DB, we use the q.querier.Choose
methodology versus q.querier.Exec
.
Subsequent, replace the HandleTodosPage
methodology in pkg/app/router.go
to question all todos and cross it to the HTML template:
func (ro *Router) HandleTodosPage(w http.ResponseWriter, r *http.Request) { todos, err := ro.todos.ListTodos(r.Context()) if err != nil { ro.logger.Error("an error occured whereas fetching todos", err) ro.rw.WriteHTMLError(w, r, cerrors.New(nil, "unable to fetch todos", map[string]interface{}{ "kind": r.Kind, })) } for i := vary todos { todos[i].Rank = int64(i + 1) } ro.rw.WriteHTML(w, r, chttp.WriteHTMLParams{ PageTemplate: "todos.html", Information: todos, }) }
To utilize the information and render a listing of all to-dos, replace internet/src/pages/todos.html
as follows:
{{ outline "content material"}} <div class="gr-container"> <h2 class="heading">Todo Checklist</h2> <kind class="add-todo" motion="/todos" methodology="submit"> <enter sort="textual content" identify="todo"> <button sort="submit">Add Todo</button> </kind> <div class="todo-list"> {{ vary . }} <kind class="todo-item" motion="/{{.Title}}" methodology="submit"> {{.Rank}}. <enter sort="textual content" identify="todo" worth="{{.Title}}"> <button class="replace" sort="submit">Replace Todo</button> <button class="delete" sort="button">Take away Todo</button> </kind> {{ finish }} </div> </div> {{ finish }}
Now, you need to see a listing of all of the to-dos within the database within the to-dos web page.
Replace to-do gadgets
To implement the replace characteristic, we’ll create a brand new SQL question to replace to-dos that exist within the database.
Replace pkg/todos/queries.go
with the next methodology:
func (q *Queries) UpdateTodo(ctx context.Context, oldName string, todo *Todo) error { const question = ` UPDATE todos SET identify=(?) WHERE identify=(?)` _, err := q.querier.Exec(ctx, question, todo.Title, oldName, ) return err }
Right here, we use an SQL question to replace a to-do worth within the DB. We cross in new and previous values respectively to the placeholder within the SQL question.
Subsequent, we’ll create a HandleUpdateTodos
methodology in pkg/app/router.go
to replace present todos:
func (ro *Router) HandleUpdateTodos(w http.ResponseWriter, r *http.Request) { var ( todo = strings.TrimSpace(r.PostFormValue("todo")) oldName = chttp.URLParams(r)["todo"] ) if todo == "" { ro.rw.WriteHTMLError(w, r, cerrors.New(nil, "unable to replace todos: todo can't be empty", map[string]interface{}{ "kind": r.Kind, })) return } newtodo := todos.Todo{Title: todo} ro.logger.WithTags(map[string]interface{}{ "oldname": oldName, "newname": newtodo.Title, }).Information("Todo up to date") err := ro.todos.UpdateTodo(r.Context(), oldName, &newtodo) if err != nil { ro.logger.Error("an error occured whereas saving todo", err) ro.rw.WriteHTMLError(w, r, cerrors.New(nil, "unable to replace todos", map[string]interface{}{ "kind": r.Kind, })) return } http.Redirect(w, r, "/todos", http.StatusSeeOther) }
Then replace Routes
with the HandleUpdateTodos
methodology as follows:
func (ro *Router) Routes() []chttp.Route { return []chttp.Route{ ... { Path: "/{todo}", Strategies: []string{http.MethodPost}, Handler: ro.HandleUpdateTodos, }, } }
Delete to-dos
To implement the delete characteristic, we’ll create a brand new SQL question to delete to-dos that exist within the database.
Replace pkg/todos/queries.go
with the next methodology:
func (q *Queries) DeleteTodo(ctx context.Context, todo *Todo) error { const question = ` DELETE from todos WHERE identify=(?)` _, err := q.querier.Exec(ctx, question, todo.Title, ) return err }
Subsequent, we’ll create HandleDeleteTodos
methodology in pkg/app/router.go
to delete present to-dos:
sort error struct { error string } func (ro *Router) HandleDeleteTodos(w http.ResponseWriter, r *http.Request) { var ( todo = strings.TrimSpace(chttp.URLParams(r)["todo"]) ) if todo == "" { deleteError := error{error: "Unable to delete todo"} ro.rw.WriteJSON(w, chttp.WriteJSONParams{StatusCode: 500, Information: deleteError}) return } newtodo := todos.Todo{Title: todo} err := ro.todos.DeleteTodo(r.Context(), &newtodo) if err != nil { ro.logger.Error("an error occured whereas deleting todo", err) deleteError := error{error: "Unable to delete todo"} ro.rw.WriteJSON(w, chttp.WriteJSONParams{StatusCode: 500, Information: deleteError}) return } http.Redirect(w, r, "/todos", http.StatusSeeOther) }
Then replace Routes
with the HandleDeleteTodos
methodology as follows:
func (ro *Router) Routes() []chttp.Route { return []chttp.Route{ ... { Path: "/{todo}", Strategies: []string{http.MethodDelete}, Handler: ro.HandleDeleteTodos, }, } }
To wrap up the delete characteristic of our to-do app, we’ll create an index.js
file in internet/public
listing and add the next operate:
operate deleteTodo(identify){ fetch(`/${identify}`, {methodology: "Delete"}).then(res =>{ if (res.standing == 200){ window.location.pathname = "/todos" } }).catch(err => { alert("An error occured whereas deleting the todo", err.message) }) }
Then, replace the take away to-do button in pages/todos.html
as follows:
<button class="delete" sort="button" onclick = "deleteTodo('{{.Title}}')" >Take away Todo</button>
We’ve got efficiently constructed an online app in Go utilizing gocopper.
In case you adopted the above tutorial accurately, your to-do app ought to be capable to carry out fundamental create, learn, replace, and delete operations.
Right here is the hyperlink to the ultimate code repository.
Conclusion
This tutorial has lastly come to an finish. We checked out how you can create an online utility in Go utilizing Copper, and we used this to construct a to-do utility efficiently.
Copper makes use of google/wire
to allow on-the-fly dependency injection. It comes with the clogger
package deal, which allows structured logging in each growth and manufacturing environments. Additionally, it really works carefully with the cerrors
package deal to assist add construction and context to your errors, leading to considerably improved error logs.
I can’t wait to see what you construct subsequent as a result of there are such a lot of methods to make this higher. You could comply with me @5x_dev on Twitter.
LogRocket: Full visibility into your internet and cell apps
LogRocket is a frontend utility monitoring resolution that allows you to replay issues as in the event that they occurred in your individual browser. As an alternative of guessing why errors occur, or asking customers for screenshots and log dumps, LogRocket permits you to replay the session to shortly perceive what went mistaken. It really works completely with any app, no matter framework, and has plugins to log further context from Redux, Vuex, and @ngrx/retailer.
Along with logging Redux actions and state, LogRocket information console logs, JavaScript errors, stacktraces, community requests/responses with headers + our bodies, browser metadata, and customized logs. It additionally devices the DOM to file the HTML and CSS on the web page, recreating pixel-perfect movies of even essentially the most complicated single-page and cell apps.