twine

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 12, 2026 License: MIT Imports: 17 Imported by: 0

README

Twine

A full-stack Go web framework for building server-side rendered applications with Alpine.js.

Features

  • Hierarchical Router: Composable routers with middleware inheritance
  • Alpine.js-First: Built-in support for Alpine Ajax requests and responses
  • Template System: Go's stdlib html/template with component-based architecture
  • Database Layer: GORM integration with migrations and generic CRUD stores
  • Authentication: JWT token generation and validation middleware
  • Error Handling: Structured errors with severity levels and stack traces
  • Logging: Configurable logging with multiple severity levels
  • Static Assets: Embedded static file serving

Installation

go install github.com/cstone/twine/cmd/twine@latest
Or Install Framework Only
go get github.com/cstone/twine

Quick Start

Using the CLI (Easiest)

Create a new project in seconds:

# Create a new project
twine init my-app

# Navigate to project directory
cd my-app

# Run the application
go run main.go

Visit http://localhost:3000 to see your app!

CLI Options
# Custom module path
twine init my-app --module github.com/myuser/my-app

# Custom port
twine init my-app --port 8080

# Minimal setup (no example pages)
twine init my-app --no-examples

# View all options
twine init --help
Manual Setup

If you prefer to set up manually:

1. Create a new project
package main

import (
    "context"
    "os"
    "os/signal"
    "syscall"

    "github.com/cstone/twine/router"
    "github.com/cstone/twine/kit"
    "github.com/cstone/twine/server"
    "github.com/cstone/twine/template"
)

func main() {
    // Load templates
    template.LoadTemplates("templates/**/*.html")

    // Create router
    r := router.NewRouter("")
    r.Get("/", Index)

    // Initialize server
    mux := r.InitializeAsRoot()
    srv := server.NewServer(":3000", mux)
    srv.Start()

    // Graceful shutdown
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()
    srv.AwaitShutdown(ctx)
}

func Index(k *kit.Kit) error {
    data := map[string]any{
        "Title": "Welcome to Twine",
    }
    return k.RenderTemplate("index", data)
}
2. Create templates
<!-- templates/pages/index.html -->
{{define "index"}}
<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}}</title>
</head>
<body>
    <h1>{{.Title}}</h1>
    {{template "button" .}}
</body>
</html>
{{end}}

<!-- templates/components/button.html -->
{{define "button"}}
<button>Click me</button>
{{end}}
3. Configure environment
# .env
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_NAME=myapp
DB_SSLMODE=disable
DB_TIMEZONE=UTC

LOGGER_LEVEL=info
LOGGER_OUTPUT=stdout
LOGGER_ERROR_OUTPUT=stderr

AUTH_SECRET=your-secret-key-here

Core Concepts

Router

The router provides hierarchical routing with middleware inheritance:

r := router.NewRouter("")

// Add middleware
r.Use(middleware.LoggingMiddleware())

// Create sub-router
api := router.NewRouter("/api")
api.Use(middleware.JWTMiddleware())

// Register routes
api.Get("/users", ListUsers)
api.Post("/users", CreateUser)
api.Get("/users/{id}", GetUser)
api.Put("/users/{id}", UpdateUser)
api.Delete("/users/{id}", DeleteUser)

// Mount sub-router
r.Sub(api)
Kit

The Kit wraps http.ResponseWriter and *http.Request for convenient access:

func Handler(k *kit.Kit) error {
    // Decode request body
    var payload UserPayload
    if err := k.Decode(&payload); err != nil {
        return err
    }

    // Get path parameters
    id := k.PathValue("id")

    // Get context values
    userID := k.GetContext("user")

    // Return JSON response
    return k.JSON(200, map[string]any{
        "message": "Success",
    })
}
Templates

Templates use Go's stdlib html/template:

// Load templates
template.LoadTemplates("templates/**/*.html")

// Render full page
func Index(k *kit.Kit) error {
    return k.RenderTemplate("index", data)
}

// Render partial (for Ajax)
func StatsPartial(k *kit.Kit) error {
    return k.RenderPartial("stats-card", stats)
}

// Auto-detect Ajax requests
func Dashboard(k *kit.Kit) error {
    // Automatically renders partial if X-Alpine-Request header is present
    return k.Render("dashboard", data)
}
Database

GORM integration with migrations and generic CRUD stores:

// Define model
type User struct {
    model.BaseModel `gorm:"embedded"`
    Name  string
    Email string
}

// Register migration
func init() {
    database.RegisterMigration(
        database.NewMigrationBuilder().
            Model(&User{}).
            Name("User").
            Build(),
    )
}

// Use CRUD store
store := store.NewCRUDStore[User](database.GORM())
users, err := store.List()
user, err := store.Get(id)
err = store.Create(user)
err = store.Update(user)
err = store.Delete(id)
Middleware

Create custom middleware:

func CustomMiddleware() middleware.Middleware {
    return func(next kit.HandlerFunc) kit.HandlerFunc {
        return func(k *kit.Kit) error {
            // Do something before
            err := next(k)
            // Do something after
            return err
        }
    }
}

Built-in middleware:

  • LoggingMiddleware(): Request logging
  • TimeoutMiddleware(duration): Request timeouts
  • JWTMiddleware(): JWT validation
Authentication

JWT token generation and validation:

// Generate token
token, err := auth.NewToken(userID, email)

// Validate token (done automatically by JWTMiddleware)
userID, err := auth.ParseToken(tokenString)

// Hash password
hash, err := auth.HashPassword(password)

// Verify password
creds := auth.Credentials{Email: email, Password: password}
err := creds.Authenticate(hashedPassword)
Error Handling

Structured errors with custom handlers:

// Use predefined errors
return errors.ErrNotFound

// Wrap errors
return errors.ErrDatabaseRead.Wrap(err)

// Add context
return errors.ErrDatabaseRead.Wrap(err).WithValue(user)

// Custom error handler
kit.UseErrorHandler(func(k *kit.Kit, err error) {
    if e, ok := err.(*errors.Error); ok {
        k.RenderTemplate("error", e)
    }
})

Alpine.js Integration

Twine is designed to work seamlessly with Alpine.js and Alpine Ajax:

<!-- Full page request -->
<a href="/dashboard">Dashboard</a>

<!-- Alpine Ajax partial request -->
<button x-target="stats" action="/stats">Refresh Stats</button>

<div id="stats">
    {{template "stats-card" .}}
</div>
func Stats(k *kit.Kit) error {
    stats := getStats()
    // Automatically returns partial for Ajax requests
    return k.Render("stats-card", stats)
}

Configuration

Configuration is loaded from environment variables and .env files:

cfg := config.Get()

// Database config
dsn := cfg.Database.DSN()

// Logger config
level := cfg.Logger.Level

// Auth config
secret := cfg.Auth.SecretKey

Project Structure

myapp/
├── main.go
├── .env
├── templates/
│   ├── pages/
│   │   └── index.html
│   ├── components/
│   │   └── button.html
│   └── layouts/
│       └── base.html
├── models/
│   └── user.go
├── handlers/
│   └── user.go
├── public/
│   └── assets/
│       ├── css/
│       └── js/
└── migrations/
    └── migrations.go

License

MIT

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Documentation

Index

Constants

View Source
const (
	GET    = router.GET
	POST   = router.POST
	PUT    = router.PUT
	DELETE = router.DELETE
)

HTTP method constants for route registration.

View Source
const (
	LogTrace    = config.LogTrace
	LogDebug    = config.LogDebug
	LogInfo     = config.LogInfo
	LogWarn     = config.LogWarn
	LogError    = config.LogError
	LogCritical = config.LogCritical
)

Log level constants.

View Source
const (
	ErrMinor    = errors.ErrMinor
	ErrError    = errors.ErrError
	ErrCritical = errors.ErrCritical
)

Error severity constants.

View Source
const (
	AssetsPath = public.AssetsPath
	PublicPath = public.PublicPath
)

Public asset path constants.

Variables

View Source
var (
	// General errors
	ErrNotFound   = errors.ErrNotFound
	ErrDecodeJSON = errors.ErrDecodeJSON
	ErrDecodeForm = errors.ErrDecodeForm

	// Database errors (most common)
	ErrDatabaseRead           = errors.ErrDatabaseRead
	ErrDatabaseWrite          = errors.ErrDatabaseWrite
	ErrDatabaseUpdate         = errors.ErrDatabaseUpdate
	ErrDatabaseDelete         = errors.ErrDatabaseDelete
	ErrDatabaseObjectNotFound = errors.ErrDatabaseObjectNotFound
	ErrDatabaseConn           = errors.ErrDatabaseConn
	ErrDatabaseMigration      = errors.ErrDatabaseMigration

	// Auth errors (most common)
	ErrAuthInvalidToken        = errors.ErrAuthInvalidToken
	ErrAuthExpiredToken        = errors.ErrAuthExpiredToken
	ErrAuthInvalidCredentials  = errors.ErrAuthInvalidCredentials
	ErrInsufficientPermissions = errors.ErrInsufficientPermissions
	ErrAuthMissingHeader       = errors.ErrAuthMissingHeader
	ErrHashPassword            = errors.ErrHashPassword
	ErrGenerateToken           = errors.ErrGenerateToken

	// API errors (most common)
	ErrAPIRequestPayload     = errors.ErrAPIRequestPayload
	ErrAPIObjectNotFound     = errors.ErrAPIObjectNotFound
	ErrAPIIDMismatch         = errors.ErrAPIIDMismatch
	ErrAPIPathValue          = errors.ErrAPIPathValue
	ErrAPIRequestContentType = errors.ErrAPIRequestContentType

	// Server errors
	ErrListenAndServe = errors.ErrListenAndServe
	ErrShutdownServer = errors.ErrShutdownServer
)

Common predefined errors for typical CRUD applications. For specialized errors, import pkg/errors directly.

View Source
var AssetsFS = &public.AssetsFS

AssetsFS should be set by the user application using //go:embed.

Example in user's code:

//go:embed assets
var AssetsFS embed.FS

func init() {
    twine.AssetsFS = AssetsFS
}

Functions

func Asset

func Asset(name string) string

Asset returns the path to a static asset.

func Config

func Config() *config.Config

Config returns the singleton configuration instance.

func DB

func DB() *gorm.DB

DB returns the underlying GORM database instance.

func FileServerHandler

func FileServerHandler() http.Handler

FileServerHandler returns an HTTP handler for serving embedded static files.

func FuncMap

func FuncMap() template.FuncMap

FuncMap returns the default template functions.

func GetTemplates

func GetTemplates() *template.Template

GetTemplates returns the current template instance.

func Handler

func Handler(h HandlerFunc) http.HandlerFunc

Handler converts a Kit.HandlerFunc to an http.HandlerFunc.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a password using bcrypt.

func LoadTemplates

func LoadTemplates(patterns ...string) error

LoadTemplates loads all templates from the given glob patterns.

func Logger

func Logger() *logger.Logger

Logger returns the singleton logger instance.

func NewMigrationBuilder

func NewMigrationBuilder() *database.MigrationBuilder

NewMigrationBuilder creates a new MigrationBuilder instance.

func NotFoundHandler

func NotFoundHandler() http.HandlerFunc

NotFoundHandler returns a handler for 404 errors.

func ParseToken

func ParseToken(tokenString string) (string, error)

ParseToken validates and parses a JWT token, returning the user ID.

func RegisterMigration

func RegisterMigration(m *database.Migration)

RegisterMigration adds a migration to the database.

func RegisterMigrations

func RegisterMigrations(ms ...*database.Migration)

RegisterMigrations adds multiple migrations to the database.

func Reload

func Reload(patterns ...string) error

Reload reloads templates from the same patterns (useful in development).

func SetAssetsFS

func SetAssetsFS(fs embed.FS)

SetAssetsFS sets the embedded filesystem for static assets.

func SetTemplates

func SetTemplates(tmpl *template.Template)

SetTemplates allows users to set a custom template instance.

func UseErrorHandler

func UseErrorHandler(h ErrorHandlerFunc)

UseErrorHandler sets a custom error handler for all Kit handlers.

Types

type AuthConfig

type AuthConfig = config.AuthConfig

AuthConfig holds authentication configuration.

type BaseModel

type BaseModel = database.BaseModel

BaseModel provides standard fields for database models.

type CRUDStore

type CRUDStore[T any] = database.CRUDStore[T]

CRUDStore provides generic CRUD operations for any model type.

func NewCRUDStore

func NewCRUDStore[T any](client *gorm.DB) *CRUDStore[T]

NewCRUDStore creates a new CRUD store for type T.

type CRUDStoreInterface

type CRUDStoreInterface[T any] = database.CRUDStoreInterface[T]

CRUDStoreInterface defines the interface for CRUD operations.

type Credentials

type Credentials = auth.Credentials

Credentials holds user authentication credentials.

type DatabaseConfig

type DatabaseConfig = config.DatabaseConfig

DatabaseConfig holds database connection settings.

type ErrSeverity

type ErrSeverity = errors.ErrSeverity

ErrSeverity represents the severity level of an error.

type Error

type Error = errors.Error

Error represents a structured application error.

type ErrorBuilder

type ErrorBuilder = errors.ErrorBuilder

ErrorBuilder provides a fluent interface for building errors.

func NewErrorBuilder

func NewErrorBuilder() *ErrorBuilder

NewErrorBuilder creates a new error builder for custom errors.

type ErrorHandlerFunc

type ErrorHandlerFunc = kit.ErrorHandlerFunc

ErrorHandlerFunc is the signature for custom error handlers.

type HandlerFunc

type HandlerFunc = kit.HandlerFunc

HandlerFunc is the signature for Twine handlers that return errors.

func ApplyMiddlewares

func ApplyMiddlewares(h HandlerFunc, middlewares ...Middleware) HandlerFunc

ApplyMiddlewares chains multiple middlewares together.

type Kit

type Kit = kit.Kit

Kit wraps http.ResponseWriter and *http.Request for convenient access.

type LogLevel

type LogLevel = config.LogLevel

LogLevel represents logging verbosity levels.

type LoggerConfig

type LoggerConfig = config.LoggerConfig

LoggerConfig holds logging configuration.

type Middleware

type Middleware = middleware.Middleware

Middleware is the signature for middleware functions.

func Chain

func Chain(middlewares ...Middleware) Middleware

Chain combines multiple middlewares into a single middleware. Useful for composing middlewares in layout files.

func JWTMiddleware

func JWTMiddleware() Middleware

JWTMiddleware validates JWT tokens and auto-redirects on failure.

func LoggingMiddleware

func LoggingMiddleware() Middleware

LoggingMiddleware logs incoming requests.

func TimeoutMiddleware

func TimeoutMiddleware(d time.Duration) Middleware

TimeoutMiddleware adds a timeout to request processing.

type Migration

type Migration = database.Migration

Migration represents a database table migration with dependencies.

type Polymorphic

type Polymorphic = database.Polymorphic

Polymorphic provides fields for polymorphic relationships.

type Router

type Router = router.Router

Router provides hierarchical routing with middleware support.

func NewRouter

func NewRouter(prefix string) *Router

NewRouter creates a new Router with the given URL prefix. The router supports hierarchical structure with middleware inheritance.

type Seeder

type Seeder = database.Seeder

Seeder provides a framework for seeding test data.

func NewSeeder

func NewSeeder(db *gorm.DB, batchSize int) *Seeder

NewSeeder creates a new Seeder instance.

type Server

type Server = server.Server

Server wraps http.Server with graceful shutdown support.

func NewServer

func NewServer(addr string, handler http.Handler) *Server

NewServer creates a new Server with the given address and handler.

type Token

type Token = auth.Token

Token represents a JWT authentication token.

func NewToken

func NewToken(userID uuid.UUID, email string) (*Token, error)

NewToken generates a new JWT token for a user.

Directories

Path Synopsis
cmd
twine command
internal
pkg
kit

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL