Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ tmp_dir = "tmp"
cmd = "make build"
delay = 1000
exclude_file = ["README.md"]
exclude_regex = ["_templ.go", "_test.go", ".db"]
exclude_unchanged = false
exclude_regex = ["_templ\\.go", "_test\\.go", "\\.db"]
exclude_dir = ["bin", "tmp", "docs", "node_modules"]
exclude_unchanged = true
follow_symlink = false
full_bin = ""
include_dir = []
Expand Down
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
# Ref: https://docs.docker.com/reference/dockerfile/#dockerignore-file
.git
node_modules
tmp
bin
*.db
docs
.air.toml
bun.lock
.deps-stamp
41 changes: 41 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4

[*.go]
indent_style = tab
tab_width = 4

[*.templ]
indent_style = tab
tab_width = 4

[*.{js,ts,jsx,tsx}]
indent_style = space
indent_size = 2

[Makefile]
indent_style = tab

[Dockerfile]
indent_style = space
indent_size = 2

[*.{md,txt}]
indent_style = space
indent_size = 2
max_line_length = off

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.css]
indent_style = space
indent_size = 2
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ tmp
node_modules
internal/web/static/css/*
!internal/web/static/css/input.css
internal/web/static/js/
internal/web/static/js/*
!internal/web/static/js/app.js

.DS_Store

Expand All @@ -14,3 +15,6 @@ internal/web/**/*_templ.go
internal/web/**/*_templ.txt

*.db

# Ignore generated swagger docs
docs/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo/
COPY --from=builder /app/bin/app /

EXPOSE 8089
EXPOSE 3000

ENTRYPOINT ["/app"]
13 changes: 9 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ all: help
## build: Compile templ files and build application
.PHONY: build
build: prepare-data
CGO_ENABLED=0 go build -ldflags="-s -w -extldflags '-static'" -trimpath -o 'bin/app' ./cmd/app
CGO_ENABLED=0 go build -tags swagger -ldflags="-s -w -extldflags '-static'" -trimpath -o 'bin/app' ./cmd/app

## start: Build and start application
.PHONY: start
start: prepare-data
go run ./cmd/app
go run -tags swagger ./cmd/app

## dev: Build and start application in live reload mode
.PHONY: dev
Expand All @@ -28,11 +28,11 @@ build-docker:
## run-docker: Run Docker container image with this app
.PHONY: run-docker
run-docker:
docker run --rm -it -p 8089:8089 $(shell basename $(PWD)):latest
docker run --rm -it -p 3000:3000 $(shell basename $(PWD)):latest

## prepare-data: Prepare data for the application
.PHONY: prepare-data
prepare-data: .deps-stamp get-js-deps generate-web
prepare-data: .deps-stamp get-js-deps generate-web generate-swagger

.deps-stamp: go.mod go.sum
go mod download
Expand Down Expand Up @@ -66,6 +66,11 @@ test: check-go
generate-web: check-go
go tool templ generate

## generate-swagger: Generate swagger documentation via swaggo/swag
.PHONY: generate-swagger
generate-swagger: check-go
go tool swag init --dir ./cmd/app,./internal/web/handlers -g main.go --parseDependency --parseInternal

## air: Build and start application in live reload mode via air
.PHONY: air
air: prepare-data
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,28 @@

Example of full-stack Go based Web app

[![GO](https://img.shields.io/badge/go-%233366CC.svg?logo=go&logoColor=white)](https://go.dev) [![templ](https://img.shields.io/badge/templ-%233366CC.svg?logo=htmx&logoColor=white)](https://github.com/a-h/templ) [![HTMX](https://img.shields.io/badge/htmx-%233366CC.svg?logo=htmx&logoColor=white)](https://github.com/bigskysoftware/htmx) [![SQLite](https://img.shields.io/badge/sqlite-%233366CC.svg?logo=sqlite&logoColor=white)](https://gitlab.com/cznic/sqlite) [![GORM](https://img.shields.io/badge/gorm-%233366CC.svg?logo=sqlite&logoColor=white)](https://github.com/go-gorm/gorm) [![tailwindcss](https://img.shields.io/badge/tailwindcss-%233366CC.svg?logo=tailwindcss&logoColor=white)](https://github.com/tailwindlabs/tailwindcss) [![daisyui](https://img.shields.io/badge/daisyui-%233366CC.svg?logo=daisyui&logoColor=white)](https://daisyui.com) [![ionicons](https://img.shields.io/badge/ionicons-%233366CC.svg?logo=ionic&logoColor=white)](https://github.com/ionic-team/ionicons)
[![GO](https://img.shields.io/badge/go-%233366CC.svg?logo=go&logoColor=white)](https://go.dev) [![fiber](https://img.shields.io/badge/fiber-%233366CC.svg?logo=go&logoColor=white)](https://github.com/gofiber/fiber) [![templ](https://img.shields.io/badge/templ-%233366CC.svg?logo=htmx&logoColor=white)](https://github.com/a-h/templ) [![HTMX](https://img.shields.io/badge/htmx-%233366CC.svg?logo=htmx&logoColor=white)](https://github.com/bigskysoftware/htmx) [![SQLite](https://img.shields.io/badge/sqlite-%233366CC.svg?logo=sqlite&logoColor=white)](https://gitlab.com/cznic/sqlite) [![GORM](https://img.shields.io/badge/gorm-%233366CC.svg?logo=sqlite&logoColor=white)](https://github.com/go-gorm/gorm) [![tailwindcss](https://img.shields.io/badge/tailwindcss-%233366CC.svg?logo=tailwindcss&logoColor=white)](https://github.com/tailwindlabs/tailwindcss) [![daisyui](https://img.shields.io/badge/daisyui-%233366CC.svg?logo=daisyui&logoColor=white)](https://daisyui.com) [![ionicons](https://img.shields.io/badge/ionicons-%233366CC.svg?logo=ionic&logoColor=white)](https://github.com/ionic-team/ionicons)


Features:
- Comfortable and flexible component based templates via [templ](https://github.com/a-h/templ)
- CRUD functionality (Create, Read, Update, and Delete entries)
- Persistent storage via [SQLite](https://gitlab.com/cznic/sqlite) + ORM ([gorm](https://github.com/go-gorm/gorm))
- User friendly interface with interactive Modals for better UX
- Error handling on server and user interface side
- Infinite Scrolling via lazy loading
- User friendly interface
- Interactive Modals for better UX
- Security configration
- Native light and dark mode support
- Preserve static files
- Swagger API documentation via [swaggo](https://github.com/swaggo/swag)

## Security

- **CSRF Protection** — session-based tokens via [Fiber](https://github.com/gofiber/fiber) middleware (Synchronizer Token Pattern)
- **SQL Injection Prevention** — all database queries use [gorm](https://github.com/go-gorm/gorm) parameterized bindings
- **Content-Security-Policy** — restricts resource loading
- **Input Validation** — server-side length limits and empty checks with field-level error reporting
- **Secure Cookies** — `HttpOnly`, `SameSite=Lax`, session-scoped for CSRF and session tokens

## Quick start

Expand All @@ -32,7 +41,7 @@ make start
make build && bin/app
```

The server starts on `:8089`.
The server starts on `:3000`.

The SQLite database is created automatically and migrations are applied on startup.

Expand Down
10 changes: 5 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

67 changes: 61 additions & 6 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,74 @@
package main

import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"

"github.com/sonjek/go-full-stack-example/internal/service"
"github.com/sonjek/go-full-stack-example/internal/storage"
"github.com/sonjek/go-full-stack-example/internal/web"
"github.com/sonjek/go-full-stack-example/internal/web/handlers"
)

// Number of notes to load per page for lazy loading
const defaultPageSize = 4

func main() {
db := storage.NewDbStorage()
storage.DBMigrate(db)
storage.SeedData(db)
db, err := storage.NewDbStorage()
if err != nil {
exitOnError("Failed to initialize database", err)
}
if err := storage.DBMigrate(db); err != nil {
exitOnError("Failed to run database migrations", err)
}
if err := storage.SeedData(db); err != nil {
exitOnError("Failed to seed database", err)
}

noteService := service.NewNoteService(db)
appHandlers := handlers.NewHandler(db, noteService)
noteService := service.NewNoteService(db, defaultPageSize)
appHandlers := handlers.NewHandler(noteService)
webServer := web.NewServer(appHandlers)
webServer.Start()
webServer.SetupMiddleware()
if err := webServer.SetupRoutes(); err != nil {
exitOnError("Failed to setup routes", err)
}

// Run web server in a goroutine for graceful shutdown
go func() {
if err := webServer.Start(); err != nil {
slog.Error("Server error", "error", err)
}
}()

// Wait for shutdown signal
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

<-ctx.Done()
stop()

slog.Info("Shutting down...")

// Create new context with timeout to let the web server finish its work
shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

if err := webServer.Shutdown(shutdownCtx); err != nil {
slog.Error("Server shutdown error", "error", err)
}

if sqlDB, err := db.DB(); err == nil {
if err := sqlDB.Close(); err != nil {
slog.Error("Failed to close database connection", "error", err)
}
}
}

func exitOnError(msg string, err error) {
slog.Error(msg, "error", err)
os.Exit(1) //nolint:revive // Helper function for main package
}
45 changes: 42 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,97 @@ go 1.26.1
require (
github.com/a-h/templ v0.3.1001
github.com/dustin/go-humanize v1.0.1
github.com/gofiber/contrib/v3/monitor v1.0.1
github.com/gofiber/contrib/v3/swaggo v1.0.1
github.com/gofiber/fiber/v3 v3.1.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/swag v1.16.6
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
modernc.org/sqlite v1.48.0
modernc.org/sqlite v1.48.1
)

require (
dario.cat/mergo v1.0.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
github.com/air-verse/air v1.63.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/bep/godartsass/v2 v2.5.0 // indirect
github.com/bep/golibsass v1.2.0 // indirect
github.com/bits-and-blooms/bitset v1.24.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cli/browser v1.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofiber/schema v1.7.0 // indirect
github.com/gofiber/utils/v2 v2.0.2 // indirect
github.com/gohugoio/hashstructure v0.6.0 // indirect
github.com/gohugoio/hugo v0.159.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.38 // indirect
github.com/mattn/go-sqlite3 v1.14.40 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/tdewolff/parse/v2 v2.8.11 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

tool (
github.com/a-h/templ/cmd/templ
github.com/air-verse/air
github.com/swaggo/swag/cmd/swag
)
Loading
Loading