aile

package module
v1.0.1 Latest Latest
Warning

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

Go to latest
Published: Mar 20, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

README

Aile - Sorry but I can't let you have the model W!

Go Reference

aile is a small stdlib-first HTTP runtime for Go. It's design is inspired in chi and hono, and it doesn't strive to be a framework, it's more like a tiny library that just adds a small amount of structure on top of the standard library API:

  • A light application container
  • Middleware Wiring
  • Graceful shutdown and signal handling

Installation

Install this library with:

$ go get -u codeberg.org/urutau-ltd/aile

Quick Start

Here's a quick usage example:

package main

import (
    "context"
    "log"
    "net/http"
    
    "codeberg.org/urutau-ltd/aile"
)

func main() {
    app, err := aile.New()
    if err != nil {
        log.Fatal(err)
    }
    
    app.Use(aile.Recovery())
    
    app.HandleFunc("GET /ping", func(w http.ResponseWriter, r *http.Request) {
        aile.Text(w, http.StatusOk, "ok")
    })
    
    // Or use the convenience helpers
    app.GET("/hello", func(w http.ResponseWriter, r *http.Request) {
        aile.Text(w, http.StatusOK, "hello")
    })
    
    if err := app.Run(context.Background()); err != nil {
        log.Fatal(err)
    }
}

Routes

aile does NOT parse or re-implement routing. It passes the literal patters you should use with http.ServeMux:

app.HandleFunc("GET /api/v1/something", handler)
app.HandleFunc("POST /users/{id}", handler)
app.HandleFunc("/plain/pattern", handler)

There are also convenience helpers:

app.GET("/users/{id}", handler)
app.POST("/users", handler)
app.DELETE("/users/{id}", handler)

Runtime

Run does the usual work for you:

  • Builds an app
  • Opens a listener
  • Serves requests
  • Listens for SIGINT and SIGTERM
  • Performs graceful shutdown

For lower-level control:

  • Build
  • Serve
  • ListenAndServe
  • Shutdown
  • Addr

Hooks

You can register startup and shutdown hooks:

app.OnStart(func(ctx context.Context, st *aile.State) error {
    return nil
})

app.OnShutdown(func(ctx context.Context, st *aile.State) error {
    return nil
})

Values

You can store small shared values on the app:

app.Set("name", "api")
v, ok := app.Value("name")

Built state receives a copy of those values.

JSON helpers

payload, err := aile.DecodeJSON[MyRequest](r)

if err != nil {
    aile.Error(w, http.StatusBadRequest, "bad json")
    return
}

_ = aile.WriteJSON(w, http.StatusOK, payload)

Extras under x/

Optional extras live under x/.

Current first-party extras:

Combine Middleware

This helper allows you to combine several middlewares as if they were one:

package main

import (
	"context"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/combine"
	"codeberg.org/urutau-ltd/aile/x/requestid"
	"codeberg.org/urutau-ltd/aile/x/secureheaders"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	stack := combine.Middleware(
		requestid.Middleware(requestid.Config{}),
		secureheaders.Middleware(secureheaders.Config{
			ContentTypeNosniff: true,
			FrameDeny:          true,
		}),
	)

	app.Use(stack)

	app.GET("/ping", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "ok")
	})

	log.Fatal(app.Run(context.Background()))
}
Cors

Easily setup your CORS headers:

package main

import (
	"context"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/cors"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(cors.Middleware(cors.Config{
		AllowOrigins: []string{"https://app.example.com"},
		AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
		AllowHeaders: []string{"Content-Type", "Authorization"},
		MaxAge:       600,
	}))

	app.GET("/api/hello", func(w http.ResponseWriter, r *http.Request) {
		_ = aile.WriteJSON(w, http.StatusOK, map[string]string{"message": "hello"})
	})

	log.Fatal(app.Run(context.Background()))
}
Request ID

Generates or propagates request id's:

 package main

import (
	"context"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/requestid"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(requestid.Middleware(requestid.Config{
		Header: "X-Request-ID",
	}))

	app.GET("/ping", func(w http.ResponseWriter, r *http.Request) {
		id, _ := requestid.FromContext(r.Context())
		_ = aile.WriteJSON(w, http.StatusOK, map[string]string{
			"request_id": id,
		})
	})

	log.Fatal(app.Run(context.Background()))
}
Secure Headers

Various security headers:

package main

import (
	"context"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/secureheaders"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(secureheaders.Middleware(secureheaders.Config{
		ContentTypeNosniff:    true,
		FrameDeny:             true,
		ReferrerPolicy:        "strict-origin-when-cross-origin",
		ContentSecurityPolicy: "default-src 'self'; style-src 'self' 'unsafe-inline'",
		HSTSMaxAge:            31536000,
		HSTSIncludeSubdomains: true,
	}))

	app.GET("/", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "secure")
	})

	log.Fatal(app.Run(context.Background()))
}
Trailing Slash

Redirect adding or removing the trailing slash of a route:

package main

import (
	"context"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/trailingslash"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(trailingslash.Middleware(trailingslash.RedirectTrim))

	app.GET("/users", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "users")
	})

	log.Fatal(app.Run(context.Background()))
}
Bearer Token validator

Simple Bearer token authentication.

package main

import (
	"context"
	"crypto/subtle"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/bearerauth"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(bearerauth.Middleware(func(token string) bool {
		return subtle.ConstantTimeCompare([]byte(token), []byte("super-token")) == 1
	}))

	app.GET("/private", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "private data")
	})

	log.Fatal(app.Run(context.Background()))}
Basic Authentication

Classic basig auth everyone knows and loves:

package main

import (
	"context"
	"crypto/subtle"
	"log"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/basicauth"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(basicauth.Middleware("admin", func(user, pass string) bool {
		userOK := subtle.ConstantTimeCompare([]byte(user), []byte("admin")) == 1
		passOK := subtle.ConstantTimeCompare([]byte(pass), []byte("secret")) == 1
		return userOK && passOK
	}))

	app.GET("/admin", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "welcome")
	})

	log.Fatal(app.Run(context.Background()))
}
GZIP Compression

[!IMPORTANT] For this to work, your clients should send the Accept-Encoding: gzip header along their requests to get compressed responses. Otherwise the compression will be skipped and the response will be served at normal size.

Compress your responses with gzip:

package main

import (
	"context"
	"log"
	"net/http"
	"strings"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/compress"
)

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(compress.Middleware(compress.Config{
		MinSize: 256,
	}))

	app.GET("/big", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, strings.Repeat("hello ", 200))
	})

	log.Fatal(app.Run(context.Background()))
}
IP (v4) restriction

Allow or Deny by IP/Network:

package main

import (
	"context"
	"log"
	"net"
	"net/http"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/iprestriction"
)

func mustCIDR(s string) *net.IPNet {
	_, n, err := net.ParseCIDR(s)
	if err != nil {
		panic(err)
	}
	return n
}

func main() {
	app, err := aile.New()
	if err != nil {
		log.Fatal(err)
	}

	app.Use(iprestriction.Middleware(iprestriction.Config{
		Allow: []*net.IPNet{
			mustCIDR("127.0.0.0/8"),
			mustCIDR("10.0.0.0/8"),
		},
		TrustProxy: false,
	}))

	app.GET("/internal", func(w http.ResponseWriter, r *http.Request) {
		aile.Text(w, http.StatusOK, "internal")
	})

	log.Fatal(app.Run(context.Background()))
}

These are convenience packages, not core concepts.

Examples

You can see more examples at the examples/ directory.

Small Blog API example

Here's how to make a blog API with aile quickly:

package main

import (
	"context"
	"log"
	"log/slog"
	"net/http"
	"time"

	"codeberg.org/urutau-ltd/aile"
	"codeberg.org/urutau-ltd/aile/x/combine"
	"codeberg.org/urutau-ltd/aile/x/cors"
	"codeberg.org/urutau-ltd/aile/x/requestid"
	"codeberg.org/urutau-ltd/aile/x/secureheaders"
	xlogger "codeberg.org/urutau-ltd/aile/x/logger"
)

type article struct {
	ID    string `json:"id"`
	Title string `json:"title"`
}

func main() {
	app, err := aile.New(aile.WithAddr(":8080"))
	if err != nil {
		log.Fatal(err)
	}

	// Example in-memory data.
	articles := []article{
		{ID: "1", Title: "Aile v1"},
		{ID: "2", Title: "Small APIs with Go"},
	}

	// Compose a practical middleware stack.
	app.Use(combine.Middleware(
		aile.Recovery(),
		requestid.Middleware(requestid.Config{
			Header: "X-Request-ID",
		}),
		xlogger.Middleware(slog.Default()),
		secureheaders.Middleware(secureheaders.Config{
			ContentTypeNosniff: true,
			FrameDeny:          true,
			ReferrerPolicy:     "strict-origin-when-cross-origin",
		}),
		cors.Middleware(cors.Config{
			AllowOrigins: []string{"http://localhost:3000"},
			AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
			AllowHeaders: []string{"Content-Type", "Authorization", "X-Request-ID"},
			MaxAge:       600,
		}),
	))

	app.GET("/healthz", func(w http.ResponseWriter, r *http.Request) {
		_ = aile.WriteJSON(w, http.StatusOK, map[string]string{
			"status": "ok",
		})
	})

	app.GET("/api/articles", func(w http.ResponseWriter, r *http.Request) {
		_ = aile.WriteJSON(w, http.StatusOK, articles)
	})

	app.POST("/api/articles", func(w http.ResponseWriter, r *http.Request) {
		var in article
		in, err = aile.DecodeJSON[article](r)
		if err != nil {
			aile.Error(w, http.StatusBadRequest, "invalid json")
			return
		}
		if in.ID == "" || in.Title == "" {
			aile.Error(w, http.StatusBadRequest, "id and title are required")
			return
		}

		articles = append(articles, in)

		_ = aile.WriteJSON(w, http.StatusCreated, in)
	})

	app.GET("/api/meta", func(w http.ResponseWriter, r *http.Request) {
		reqID, _ := requestid.FromContext(r.Context())

		_ = aile.WriteJSON(w, http.StatusOK, map[string]string{
			"service":    "articles",
			"request_id": reqID,
		})
	})

	log.Fatal(app.Run(context.Background()))
}

Contributing

See CONTRIBUTING.md

GNU Guix compatiblity

This project is being developed in GNU Guix. And thus the supported Go version is the latest available on GNU Guix. You may use some utilities present in this repository if you wish to contribute using guix:

  • make env should create a new guix shell environment with the necessary development dependencies.

  • make pkg should test if the guix.scm package definition of this library builds and installs correctly under GNU Guix.

COPYING

Where applicable this project's source code is under the terms of the GNU Affero General Public License version 3 or at your option, any later version.

Documentation

Overview

Package aile provides a small stdlib-first HTTP runtime for Go.

aile keeps net/http front and center like chi does. it uses http.ServeMux http.Server, http.Handler and literal stlib route patterns such as:

"GET /api/v1/example" "POST /users/{id}"

Aile mainly adds:

  • A named build plan
  • Patchable phases
  • A small application container
  • Runtime helpers for signal handling and graceful shutdown

Index

Constants

View Source
const (
	// The HTTP server timeout for read operations duration in seconds.
	// if not set in the application configuration this constant will be used
	// as the fallback value.
	//
	// This is used in the ReadTimeout and ReadHeaderTimeout [Config] fields.
	READ_TIMEOUT time.Duration = 5 * time.Second

	// The HTTP server timeout duration for write operations duration in
	// seconds.
	// if not set in the application configuration this constant will be
	// used as the fallback value.
	//
	// This is used in the WriteTimeout and ShutdownTimeout [Config] fields.
	WRITE_TIMEOUT time.Duration = 10 * time.Second
	LONG_TIMEOUT  time.Duration = 60 * time.Second
)

Variables

This section is empty.

Functions

func DecodeJSON

func DecodeJSON[T any](r *http.Request) (T, error)

Decode JSON request body into a given type T.

func Error

func Error(w http.ResponseWriter, status int, msg string)

Writes an HTTP error response.

func Status

func Status(w http.ResponseWriter, status int)

Writes a status code with no request body.

func Text

func Text(w http.ResponseWriter, status int, body string)

Writes a Plain Text HTTP Response

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any) error

Writes a JSON HTTP Response. It already sets the Content-Type header and the charset is set to uft-8. Mind this to avoid header duplication in your code.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App is the main application container.

App is safe (at least I think so) for concurrent reads after construction, but configuration methods such as aile/Handle, aile/Use, aile/Set, aile/OnStart and aile/OnShutdown should be called before starting the server itself.

Configure the app before starting the server. Mutating routes, middleware, values or hooks while serving is NOT supported.

func MustNew

func MustNew(opts ...Option) *App

Same as New but it either builds a new App or the program panics.

func New

func New(opts ...Option) (*App, error)

App constructor function.

func (*App) Addr

func (a *App) Addr() string

func (*App) Build

func (a *App) Build(ctx context.Context) (*State, error)

Build constructs the App into a State without starting the server.

func (*App) DELETE

func (a *App) DELETE(pattern string, h http.HandlerFunc)

func (*App) GET

func (a *App) GET(pattern string, h http.HandlerFunc)

GET registers a GET route in an App.

func (*App) HEAD

func (a *App) HEAD(pattern string, h http.HandlerFunc)

func (*App) Handle

func (a *App) Handle(pattern string, h http.Handler)

Handle registers a literal stdlib ServeMux patter and handler.

func (*App) HandleFunc

func (a *App) HandleFunc(pattern string, h http.HandlerFunc)

HandleFunc registers a literal stdlib ServeMux pattern and HandlerFunc.

func (*App) ListenAndServe

func (a *App) ListenAndServe() error

func (*App) OPTIONS

func (a *App) OPTIONS(pattern string, h http.HandlerFunc)

func (*App) OnShutdown

func (a *App) OnShutdown(fn HookFunc)

Use OnShutdown to append a shutdown hook.

func (*App) OnStart

func (a *App) OnStart(fn HookFunc)

Use OnStart to append a startup hook.

func (*App) PATCH

func (a *App) PATCH(pattern string, h http.HandlerFunc)

func (*App) POST

func (a *App) POST(pattern string, h http.HandlerFunc)

POST registers a POST route in an App.

func (*App) PUT

func (a *App) PUT(pattern string, h http.HandlerFunc)

PUT registers a PUT route in an App.

func (*App) Run

func (a *App) Run(ctx context.Context) error

func (*App) Serve

func (a *App) Serve(ln net.Listener) error

func (*App) Set

func (a *App) Set(name string, value any)

Set stores a named value on the App.

func (*App) Shutdown

func (a *App) Shutdown(ctx context.Context) error

Attempt to shutdown gracefully a currently running aile/App. if the app is not running, Shutdown returns nil.

func (*App) Use

func (a *App) Use(mw ...Middleware)

Use appends middleware to an existing App instance

func (*App) Value

func (a *App) Value(name string) (any, bool)

Value searches for a named value from the app.

type Config

type Config struct {
	// An HTTP server address to serve the aile [App].
	Addr              string
	ReadTimeout       time.Duration
	ReadHeaderTimeout time.Duration
	WriteTimeout      time.Duration
	IdleTimeout       time.Duration
	ShutdownTimeout   time.Duration
	MaxHeaderBytes    int
}

Config controls the HTTP server runtime.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the default runtime configuration.

type HookFunc

type HookFunc func(context.Context, *State) error

HookFunc runs during startup or shutdown.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is the standard net/http middleware shape.

func Recovery

func Recovery() Middleware

type Option

type Option func(*App) error

The Option function configures an aile/App during construction

func WithAddr

func WithAddr(addr string) Option

WithAddr sets the server listen address, overriding the default one which is ":9001"

func WithConfig

func WithConfig(cfg Config) Option

WithConfig replaces the full app configuration. Zero values are later filled using aile/DefaultConfig during Build/Run.

func WithMiddleware

func WithMiddleware(mw ...Middleware) Option

WithMiddleware appends middleware which are http/HandlerFunction during construction

type State

type State struct {
	Config  Config
	Mux     *http.ServeMux
	Handler http.Handler
	Server  *http.Server
	Values  map[string]any
}

State is the built runtime state for a given aile/App

Directories

Path Synopsis
examples
htmx-counter command
rest-api command
x

Jump to

Keyboard shortcuts

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