652 lines
23 KiB
Markdown
652 lines
23 KiB
Markdown
---
|
||||
|
|
page-title: "How to build a fullstack application with Go, Templ, and HTMX - DEV Community"
|
|||
|
|
url: https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444
|
|||
|
|
date: "2024-07-02 21:52:39"
|
|||
|
|
---
|
|||
|
|
Go is a statically typed, compiled high-level programming language for building systems, command-line interfaces (CLI), and more. It is typically designed for use on the backend; however, there are times when you want to use the same language to build a full-stack application with a functional backend and a visual frontend.
|
|||
|
|
|
|||
|
|
In most cases, Go developers opt for frontend frameworks/libraries like React, Vue, Angular, etc., to build the frontend part of the application. This means they must learn JavaScript/TypeScript, framework-specific paradigms, and other frontend-related overheads.
|
|||
|
|
|
|||
|
|
In this guide, you’ll learn how to build a fullstack application with Go using Templ, HTMX, and Xata.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#technology-overview)Technology Overview
|
|||
|
|
|
|||
|
|
**Templ**: is a templating engine that lets you build HTML with Go. It also lets you use Go syntax like `if`, `switch`, and `for` statements to build a robust frontend. You will use Templ to build reusable components and pages for the frontend.
|
|||
|
|
|
|||
|
|
**HTMX**: is a frontend library that lets you access modern browser features directly using HTML rather than JavaScript. You will use HTMX to process the form submission and perform other dynamic operations.
|
|||
|
|
|
|||
|
|
**Xata**: is a serverless database with analytics and free-text search support that makes a wide range of applications easy to build.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#prerequisites)Prerequisites
|
|||
|
|
|
|||
|
|
To follow along with this tutorial, the following are needed:
|
|||
|
|
|
|||
|
|
- [Go version 1.20 or higher installed](https://go.dev/dl/)
|
|||
|
|
- Basic understanding of Go
|
|||
|
|
- Xata account. [Signup is free](https://app.xata.io/signin?mode=signup?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#getting-started)Getting started
|
|||
|
|
|
|||
|
|
To get started, you need to install Templ binary. The binary generates Go code from a Templ file.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go install github.com/a-h/templ/cmd/templ@latest
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
Create a directory.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
mkdir go_fullstack && cd go_fullstack
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
Next, initialize a Go module to manage project dependencies.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go mod init go-fullstack
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
Finally, we proceed to install the required dependencies with:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go get github.com/gin-gonic/gin github.com/a-h/templ github.com/joho/godotenv
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
`github.com/gin-gonic/gin` is a framework for building web applications.
|
|||
|
|
|
|||
|
|
`github.com/a-h/templ` is the Templ library used in the project.
|
|||
|
|
|
|||
|
|
`github.com/joho/godotenv` is a library for loading environment variables.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#structuring-the-application)Structuring the application
|
|||
|
|
|
|||
|
|
To do this, create a `cmd`, `internals`, and `views` folder in our project directory.
|
|||
|
|
|
|||
|
|
`cmd` is for structuring the application entry point.
|
|||
|
|
|
|||
|
|
`internals` is for structuring API-related files.
|
|||
|
|
|
|||
|
|
`views` is for structuring frontend-related files.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#setup-the-database-on-xata)Setup the database on Xata
|
|||
|
|
|
|||
|
|
Log into the [Xata workspace](https://app.xata.io/workspaces) and create a `todo` database. Inside the `todo` database, create a `Todo` table and add a `description` column of type `String`.
|
|||
|
|
|
|||
|
|
[](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnohlapfqs2x519j2klo.png)
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#get-the-database-url-and-set-up-the-api-key)Get the Database URL and set up the API Key
|
|||
|
|
|
|||
|
|
To get the database URL, click the **Get code snippet** button and copy the URL. Then click the **API Key** link, add a new key, save and copy the API key.
|
|||
|
|
|
|||
|
|
[](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpgs4uphc5u9sewh2wf86.png)
|
|||
|
|
[](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffhi01luxfz88eh9hy5cm.png)
|
|||
|
|
|
|||
|
|
**Setup environment variable**
|
|||
|
|
|
|||
|
|
Create a `.env` file in the root directory and add the copied URL and API key.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
XATA_DATABASE_URL= <REPLACE WITH THE COPIED DATABASE URL>
|
|||
|
|
XATA_API_KEY=<REPLACE WITH THE COPIED API KEY>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#build-the-application-frontend)Build the application Frontend
|
|||
|
|
|
|||
|
|
To build the frontend, you’ll use Templ and HTMX to structure the application and add dynamism to a Todo application.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#create-the-application-components)Create the application components
|
|||
|
|
|
|||
|
|
Inside the `views` folder, create a `components/header.templ` file and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package components
|
|||
|
|
|
|||
|
|
templ Header() {
|
|||
|
|
<head>
|
|||
|
|
<script
|
|||
|
|
src="https://unpkg.com/htmx.org@1.9.10"
|
|||
|
|
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
|
|||
|
|
crossorigin="anonymous"
|
|||
|
|
></script>
|
|||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|||
|
|
<meta charset="UTF-8" />
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|||
|
|
<title>GO Fullstack</title>
|
|||
|
|
</head>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet creates a `Header` component and adds HTMX and TailwindCSS CDNs. TailwindCSS is a low-level framework for styling.
|
|||
|
|
|
|||
|
|
Next, create a `components/footer.templ` file to create the application footer and style using TailwindCSS classes.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package components
|
|||
|
|
|
|||
|
|
templ Footer() {
|
|||
|
|
<footer class="fixed p-1 bottom-0 bg-gray-100 w-full border-t">
|
|||
|
|
<div class="rounded-lg p-4 text-xs italic text-gray-700 text-center">
|
|||
|
|
© Go Fullstack
|
|||
|
|
</div>
|
|||
|
|
</footer>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
Finally, create an `index.templ` file inside the same `views` folder and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package views
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"fmt"
|
|||
|
|
"go_fullstack/views/components"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type Todo struct {
|
|||
|
|
Id string
|
|||
|
|
Description string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
templ Index(todos []*Todo) {
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en">
|
|||
|
|
@components.Header()
|
|||
|
|
<body>
|
|||
|
|
<main class="min-h-screen w-full">
|
|||
|
|
<nav class="flex w-full border border-b-zinc-200 px-4 py-4">
|
|||
|
|
<h3 class="text-base lg:text-lg font-medium text-center">
|
|||
|
|
GO Fullstack app
|
|||
|
|
</h3>
|
|||
|
|
</nav>
|
|||
|
|
<div class="mt-6 w-full flex justify-center items-center flex-col">
|
|||
|
|
// FORM PROCESSING
|
|||
|
|
<form
|
|||
|
|
hx-post="/"
|
|||
|
|
hx-trigger="submit"
|
|||
|
|
hx-swap="none"
|
|||
|
|
onsubmit="reloadPage()"
|
|||
|
|
class="w-96"
|
|||
|
|
>
|
|||
|
|
<textarea
|
|||
|
|
name="description"
|
|||
|
|
cols="30"
|
|||
|
|
rows="2"
|
|||
|
|
class="w-full border rounded-lg mb-2 p-4"
|
|||
|
|
placeholder="Input todo details"
|
|||
|
|
required
|
|||
|
|
></textarea>
|
|||
|
|
<button
|
|||
|
|
class="py-1 px-4 w-full h-10 rounded-lg text-white bg-zinc-800"
|
|||
|
|
>
|
|||
|
|
Create
|
|||
|
|
</button>
|
|||
|
|
</form>
|
|||
|
|
<section class="border-t border-t-zinc-200 mt-6 px-2 py-4 w-96">
|
|||
|
|
// LOOP THROUGH THE TODOS
|
|||
|
|
<ul id="todo-list">
|
|||
|
|
for _, todo := range todos {
|
|||
|
|
<li class="ml-4 ml-4 border p-2 rounded-lg mb-2" id={ fmt.Sprintf("%s", todo.Id) }>
|
|||
|
|
<p class="font-medium text-sm">Todo item { todo.Id }</p>
|
|||
|
|
<p class="text-sm text-zinc-500 mb-2">
|
|||
|
|
{ todo.Description }
|
|||
|
|
</p>
|
|||
|
|
<div class="flex gap-4 items-center mt-2">
|
|||
|
|
<a
|
|||
|
|
href="#"
|
|||
|
|
class="flex items-center border py-1 px-2 rounded-lg"
|
|||
|
|
>
|
|||
|
|
<p class="text-sm">Edit</p>
|
|||
|
|
</a>
|
|||
|
|
<button
|
|||
|
|
hx-delete={ fmt.Sprintf("/%s", todo.Id) }
|
|||
|
|
hx-swap="delete"
|
|||
|
|
hx-target={ fmt.Sprintf("#%s", todo.Id) }
|
|||
|
|
class="flex items-center border py-1 px-2 rounded-lg hover:bg-red-300"
|
|||
|
|
>
|
|||
|
|
<p class="text-sm">Delete</p>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</li>
|
|||
|
|
}
|
|||
|
|
</ul>
|
|||
|
|
</section>
|
|||
|
|
</div>
|
|||
|
|
</main>
|
|||
|
|
</body>
|
|||
|
|
@components.Footer()
|
|||
|
|
</html>
|
|||
|
|
<script>
|
|||
|
|
function reloadPage() {
|
|||
|
|
setTimeout(function() {
|
|||
|
|
window.location.reload();
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above does the following:
|
|||
|
|
|
|||
|
|
- Imports the required dependencies
|
|||
|
|
- Creates a `Todo` struct to represent the response data coming from the backend
|
|||
|
|
- Creates an `Index` component that uses the `Header` and `Footer` components to structure the page. Then, it uses the HTMX attributes to process form submissions and deletion of todos by calling the respective endpoints `/` and `/{todo.Id}`
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#generating-go-files-from-the-templ-files)Generating Go files from the Templ files
|
|||
|
|
|
|||
|
|
Next, use the Templ binary you installed earlier to Generate Go codes from the views created above by running the command below in your terminal:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
templ generate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
After you run this command, you’ll see new Go files generated for each view. You use generated file to render your frontend in next section.
|
|||
|
|
|
|||
|
|
[](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0n91b8w6mchs03qq761h.png)
|
|||
|
|
|
|||
|
|
> The generated files are not to be edited.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#putting-it-together-and-building-the-backend)Putting it together and building the backend
|
|||
|
|
|
|||
|
|
With that done, you can use it to build the backend and render the required page.
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#create-the-api-models-and-helper-function)Create the API models and helper function
|
|||
|
|
|
|||
|
|
To represent the application data, create a `model.go` file inside the `internals` folder and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package internals
|
|||
|
|
|
|||
|
|
type Todo struct {
|
|||
|
|
Id string `json:"id,omitempty"`
|
|||
|
|
Description string `json:"description,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TodoRequest struct {
|
|||
|
|
Description string `json:"description,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TodoResponse struct {
|
|||
|
|
Id string `json:"id,omitempty"`
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above creates a `Todo`, `TodoRequest`, and `TodoResponse` struct with the required properties to describe requests and response types.
|
|||
|
|
|
|||
|
|
Finally, create a `helpers.go` file with a reusable function to load environment variables.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package internals
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"log"
|
|||
|
|
"os"
|
|||
|
|
"github.com/joho/godotenv"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func GetEnvVariable(key string) string {
|
|||
|
|
err := godotenv.Load()
|
|||
|
|
if err != nil {
|
|||
|
|
log.Fatal("Error loading .env file")
|
|||
|
|
}
|
|||
|
|
return os.Getenv(key)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#create-the-application-and-api-routes)Create the application and API routes
|
|||
|
|
|
|||
|
|
Create a `route.go` file for configuring the API routes and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package api
|
|||
|
|
|
|||
|
|
import "github.com/gin-gonic/gin"
|
|||
|
|
|
|||
|
|
type Config struct {
|
|||
|
|
Router *gin.Engine
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) Routes() {
|
|||
|
|
//routes will come here
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above does the following:
|
|||
|
|
|
|||
|
|
- Imports the required dependency
|
|||
|
|
- Creates a `Config` struct with a `Router` property to configure the application methods
|
|||
|
|
- Creates a `Routes` function that takes in the `Config` struct as a pointer
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#create-the-api-services)Create the API services
|
|||
|
|
|
|||
|
|
With that done, create a `xata_service.go` file for the application and update it by doing the following:
|
|||
|
|
|
|||
|
|
First, import the required dependencies and create a helper function:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package internals
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bytes"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"net/http"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var xataAPIKey = GetEnvVariable("XATA_API_KEY")
|
|||
|
|
var baseURL = GetEnvVariable("XATA_DATABASE_URL")
|
|||
|
|
|
|||
|
|
func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
|
|||
|
|
var req *http.Request
|
|||
|
|
var err error'
|
|||
|
|
|
|||
|
|
if method == "GET" || method == "DELETE" || bodyData == nil {
|
|||
|
|
req, err = http.NewRequest(method, url, nil)
|
|||
|
|
} else {
|
|||
|
|
req, err = http.NewRequest(method, url, bodyData)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req.Header.Add("Content-Type", "application/json")
|
|||
|
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", xataAPIKey))
|
|||
|
|
|
|||
|
|
return req, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above does the following:
|
|||
|
|
|
|||
|
|
- Imports the required dependencies
|
|||
|
|
- Creates required environment variables
|
|||
|
|
- Creates a `createRequest` function that creates HTTP requests with the required headers
|
|||
|
|
|
|||
|
|
Lastly, add a `createTodoService`, `deleteTodoService`, and `getAllTodosService` methods to create, delete, and get the list of todos.
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
//imports goes here
|
|||
|
|
|
|||
|
|
func createRequest(method, url string, bodyData *bytes.Buffer) (*http.Request, error) {
|
|||
|
|
//createRequest code goes here
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) createTodoService(newTodo *TodoRequest) (*TodoResponse, error) {
|
|||
|
|
createTodo := TodoResponse{}
|
|||
|
|
jsonData := Todo{
|
|||
|
|
Description: newTodo.Description,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
bodyData := new(bytes.Buffer)
|
|||
|
|
json.NewEncoder(bodyData).Encode(jsonData)
|
|||
|
|
|
|||
|
|
fullURL := fmt.Sprintf("%s:main/tables/Todo/data", baseURL)
|
|||
|
|
req, err := createRequest("POST", fullURL, bodyData)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
client := &http.Client{}
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if err := json.NewDecoder(resp.Body).Decode(&createTodo); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &createTodo, nil
|
|||
|
|
}
|
|||
|
|
func (app *Config) deleteTodoService(id string) (string, error) {
|
|||
|
|
fullURL := fmt.Sprintf("%s:main/tables/Todo/data/%s", baseURL, id)
|
|||
|
|
client := &http.Client{}
|
|||
|
|
req, err := createRequest("DELETE", fullURL, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
return id, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) getAllTodosService() ([]*Todo, error) {
|
|||
|
|
var todos []*Todo
|
|||
|
|
|
|||
|
|
fullURL := fmt.Sprintf("%s:main/tables/Todo/query", baseURL)
|
|||
|
|
client := &http.Client{}
|
|||
|
|
req, err := createRequest("POST", fullURL, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resp, err := client.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
var response struct {
|
|||
|
|
Records []*Todo `json:"records"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
decoder := json.NewDecoder(resp.Body)
|
|||
|
|
if err := decoder.Decode(&response); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
todos = response.Records
|
|||
|
|
return todos, nil
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#create-the-api-handlers)Create the API handlers
|
|||
|
|
|
|||
|
|
With that done, you can use the services to create the API handlers. Create a `handler.go` file inside `internals` folder and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package internals
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"go_fullstack/views"
|
|||
|
|
"net/http"
|
|||
|
|
"time"
|
|||
|
|
"github.com/a-h/templ"
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const appTimeout = time.Second * 10
|
|||
|
|
|
|||
|
|
func render(ctx *gin.Context, status int, template templ.Component) error {
|
|||
|
|
ctx.Status(status)
|
|||
|
|
return template.Render(ctx.Request.Context(), ctx.Writer)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) indexPageHandler() gin.HandlerFunc {
|
|||
|
|
return func(ctx *gin.Context) {
|
|||
|
|
_, cancel := context.WithTimeout(context.Background(), appTimeout)
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
todos, err := app.getAllTodosService()
|
|||
|
|
if err != nil {
|
|||
|
|
ctx.JSON(http.StatusBadRequest, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var viewsTodos []*views.Todo
|
|||
|
|
for _, todo := range todos {
|
|||
|
|
viewsTodo := &views.Todo{
|
|||
|
|
Id: todo.Id,
|
|||
|
|
Description: todo.Description,
|
|||
|
|
}
|
|||
|
|
viewsTodos = append(viewsTodos, viewsTodo)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
render(ctx, http.StatusOK, views.Index(viewsTodos))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) createTodoHandler() gin.HandlerFunc {
|
|||
|
|
return func(ctx *gin.Context) {
|
|||
|
|
_, cancel := context.WithTimeout(context.Background(), appTimeout)
|
|||
|
|
description := ctx.PostForm("description")
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
newTodo := TodoRequest{
|
|||
|
|
Description: description,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
data, err := app.createTodoService(&newTodo)
|
|||
|
|
if err != nil {
|
|||
|
|
ctx.JSON(http.StatusBadRequest, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.JSON(http.StatusCreated, data)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) deleteTodoHandler() gin.HandlerFunc {
|
|||
|
|
return func(ctx *gin.Context) {
|
|||
|
|
_, cancel := context.WithTimeout(context.Background(), appTimeout)
|
|||
|
|
id := ctx.Param("id")
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
data, err := app.deleteTodoService(id)
|
|||
|
|
if err != nil {
|
|||
|
|
ctx.JSON(http.StatusBadRequest, err.Error())
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ctx.JSON(http.StatusAccepted, fmt.Sprintf("Todo with ID: %s deleted successfully!!", data))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above does the following
|
|||
|
|
|
|||
|
|
- Imports the required dependencies
|
|||
|
|
- Creates a `render` function that uses the `Templ` package to render matching template
|
|||
|
|
- Creates an `indexPageHandler` function that returns a Gin-gonic handler and takes in the `Config` struct as a pointer. Inside the returned handler, use the `getAllTodosService` service to get the list of todos and then render the appropriate page using the generated code from the views package (frontend)
|
|||
|
|
- Creates a `createdTodoHandler` and `deleteProjectHandler` functions that return a Gin-gonic handler and take in the `Config` struct as a pointer. Use the service created earlier to perform the corresponding action inside the returned handler
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#update-the-api-routes-to-use-handlers)Update the API routes to use handlers
|
|||
|
|
|
|||
|
|
Update the `routes.go` file with the handlers as shown below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package internals
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type Config struct {
|
|||
|
|
Router *gin.Engine
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (app *Config) Routes() {
|
|||
|
|
//views
|
|||
|
|
app.Router.GET("/", app.indexPageHandler())
|
|||
|
|
|
|||
|
|
//apis
|
|||
|
|
app.Router.POST("/", app.createTodoHandler())
|
|||
|
|
app.Router.DELETE("/:id", app.deleteTodoHandler())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#putting-it-all-together)Putting it all together
|
|||
|
|
|
|||
|
|
Create the application entry point to use to serve the routes. To do this, create a `main.go` file inside the `cmd` folder and add the snippet below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"go_fullstack/internals"
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func main() {
|
|||
|
|
router := gin.Default()
|
|||
|
|
|
|||
|
|
//initialize config
|
|||
|
|
app := internals.Config{Router: router}
|
|||
|
|
|
|||
|
|
//routes
|
|||
|
|
app.Routes()
|
|||
|
|
|
|||
|
|
router.Run(":8080")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|
The snippet above does the following:
|
|||
|
|
|
|||
|
|
- Imports the required dependencies
|
|||
|
|
- Creates a Gin router using the `Default` configuration
|
|||
|
|
- Initialize the `Config` struct by passing in the `Router`
|
|||
|
|
- Adds the route and run the application on port `:8080`
|
|||
|
|
|
|||
|
|
With that done, you can start a development server using the command below:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
go run cmd/main.go
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enter fullscreen mode Exit fullscreen mode
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
The complete source code can be found on [GitHub](https://github.com/Mr-Malomz/go_fullstack).
|
|||
|
|
|
|||
|
|
## [](https://dev.to/hackmamba/how-to-build-a-fullstack-application-with-go-templ-and-htmx-4444#conclusion)Conclusion
|
|||
|
|
|
|||
|
|
This post discusses how to build a fullstack application with Go, Templ, HTMX, and Xata. You can extend the application further to support viewing and editing todos.
|
|||
|
|
|
|||
|
|
These resources may also be helpful:
|
|||
|
|
|
|||
|
|
- [Xata documentation](https://xata.io/docs?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
|
|||
|
|
- [Templ documentation](https://templ.guide/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
|
|||
|
|
- [HTMX documentation](https://htmx.org/docs/?utm_source=fullstackwriter&utm_medium=fullstackwriter-blog)
|
|||
|
|
- [Go + Xata](https://fullstackwriter.dev/post/xata-go-a-getting-started-guide?category=Golang)
|