m

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2025 License: MIT Imports: 13 Imported by: 0

README

Mint

A lightweight, type-safe Go web framework built on top of net/http with automatic parameter extraction and elegant response handling.

✨ Features

  • 🚀 Zero Learning Curve - Built on standard net/http, no custom router
  • 🎯 Automatic Parameter Extraction - JSON body, query params, form data, and path parameters
  • Automatic Validation - Built-in validation with user-friendly error messages
  • 🔒 Type-Safe - Leverages Go generics for compile-time type safety
  • 📦 Flexible Response Handling - Return any type: structs, strings, HTML, status codes, or custom results
  • Minimal Boilerplate - Write handlers as simple functions
  • 🛠️ Customizable - Configure JSON encoding, schema decoding, and error handling
  • 🪶 Lightweight - No dependencies beyond gorilla/schema for form parsing

📦 Installation

go get github.com/cymoo/mint

Requirements: Go 1.23+ (for enhanced routing patterns)

🚀 Quick Start

package main

import (
	"net/http"

	"github.com/cymoo/mint"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

type UpdateUserRequest struct {
	Name string `json:"name" validate:"required,min=2,max=50"`
}

// Simple string response
func handleHome() string {
	return "Hello, World!"
}

// JSON response with path parameter
func handleGetUser(id m.Path[int]) (User, error) {
	// id.Value contains the parsed integer
	return User{ID: id.Value, Name: "Alice"}, nil
}

// Result[T] for full control over the response with multiple parameters with different types
func handleUpdateUser(id m.Path[int], req m.JSON[UpdateUserRequest]) m.Result[*User] {
	return m.Result[*User]{
		Data: &User{ID: id.Value, Name: req.Value.Name},
		Code: http.StatusOK,
		Headers: http.Header{
			"X-Custom-Header": []string{"foo"},
		},
	}
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("/", m.H(handleHome))
	mux.HandleFunc("GET /users/{id}", m.H(handleGetUser))
	mux.HandleFunc("PUT /users/{id}", m.H(handleUpdateUser))

	err := http.ListenAndServe(":8080", mux)
	if err != nil {
		panic(err)
	}
}

See _examples for more detailed examples.

📚 Core Concepts

The H Function

m.H() wraps your handler functions, enabling automatic parameter extraction and response handling:

mux.HandleFunc("POST /users", m.H(handleCreateUser))
Parameter Extractors

Extract data from requests using type-safe extractors:

Extractor Purpose Example
m.Path[T] Path parameters {id}m.Path[int]
m.JSON[T] JSON request body m.JSON[CreateUserRequest]
m.Query[T] Query parameters ?page=1m.Query[Pagination]
m.Form[T] Form data username=...m.Form[LoginForm]
Response Types

Return values are automatically handled:

Return Type Result
string text/plain response
m.HTML text/html response
struct / map / slice application/json response
m.StatusCode HTTP status code only
[]byte application/octet-stream response
m.Result[T] Custom status code + headers + data
error Automatic error handling
(T, error) Data or error pattern

📖 Usage Examples

Path Parameters

Extract typed path parameters from URLs:

// Single parameter
mux.HandleFunc("GET /users/{id}", m.H(func(id m.Path[int]) (User, error) {
    user, ok := getUser(id.Value)
    if !ok {
        return User{}, &m.HTTPError{
            Code:    404,
            Err:     "not_found",
            Message: "user not found",
        }
    }
    return user, nil
}))

// Multiple parameters with different types
mux.HandleFunc("GET /calc/{a}/{b}", m.H(func(a m.Path[int], b m.Path[float64]) map[string]any {
    return map[string]any{
        "sum": float64(a.Value) + b.Value,
    }
}))

Supported types: string, int, int64, uint, uint64, float64, bool

JSON Request Body

Parse JSON request bodies automatically:

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

mux.HandleFunc("POST /users", m.H(func(body m.JSON[CreateUserRequest]) m.Result[User] {
    user := User{
        Name:  body.Value.Name,
        Email: body.Value.Email,
    }
    
    return m.Result[User]{
        Code: 201,
        Headers: http.Header{
            "Location": []string{"/users/" + user.ID},
        },
        Data: user,
    }
}))
Query Parameters

Extract and parse query parameters:

type Pagination struct {
    Page  int    `schema:"page"`
    Limit int    `schema:"limit"`
    Sort  string `schema:"sort"`
}

mux.HandleFunc("GET /users", m.H(func(q m.Query[Pagination]) []User {
    // Access via q.Value.Page, q.Value.Limit, etc.
    return getUsers(q.Value.Page, q.Value.Limit)
}))
Form Data

Parse form submissions:

type LoginForm struct {
    Username string `schema:"username"`
    Password string `schema:"password"`
}

mux.HandleFunc("POST /login", m.H(func(form m.Form[LoginForm]) map[string]string {
    // Authenticate user
    token := authenticate(form.Value.Username, form.Value.Password)
    return map[string]string{"token": token}
}))
Custom Response with Headers

Use m.Result[T] for full control over the response:

mux.HandleFunc("GET /download", m.H(func() m.Result[Data] {
    return m.Result[Data]{
        Code: 200,
        Headers: http.Header{
            "Content-Disposition": []string{"attachment; filename=data.json"},
            "X-Custom-Header":     []string{"value"},
        },
        Data: myData,
    }
}))
Error Handling

Multiple ways to handle errors:

// 1. Return generic error (status inferred from message)
func handler() error {
    return errors.New("not found") // → 404
}

// 2. Return custom HTTP error
func handler() error {
    return &m.HTTPError{
        Code:    400,
        Err:     "validation_error",
        Message: "invalid input",
    }
}

// 3. Two-value return pattern
func handler(id m.Path[int]) (User, error) {
    user, err := getUser(id.Value)
    if err != nil {
        return User{}, err
    }
    return user, nil
}

// 4. Result with error
func handler() m.Result[User] {
    return m.Err[User](400, errors.New("bad request"))
}
Direct HTTP Access

When you need full control, access raw HTTP primitives:

mux.HandleFunc("GET /custom", m.H(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("X-Custom", "header")
    w.WriteHeader(200)
    w.Write([]byte("custom response"))
}))

Custom Extractors Guide

Custom extractors allow you to extend the framework to handle any type of request data. Here's how to create your own:

Basic Structure

Implement the Extractor interface with an Extract method:

type BearerToken struct {
    Token string
}

func (bt *BearerToken) Extract(r *http.Request) error {
    const bearerPrefix = "Bearer "
    auth := r.Header.Get("Authorization")
    
    if strings.HasPrefix(auth, bearerPrefix) {
        bt.Token = strings.TrimSpace(auth[len(bearerPrefix):])
    }
    
    if bt.Token == "" {
        return &m.ExtractError{
            Type:    "invalid_authorization",
            Message: "Authorization header must be: Bearer <token>",
        }
    }
    
    return nil
}
Key Features
  • Automatic Injection: The framework automatically calls Extract() and injects the parsed value
  • Error Handling: Return ExtractError with clear type and message for client errors
  • Type Safety: Leverage Go's type system for validated, type-safe parameters
Usage in Handlers

Simply include your custom extractor as a handler parameter:

func mySecureApi(bearer BearerToken) string {
    return "token: " + bearer.Token
}
Best Practices
  • Keep extractors focused on single responsibility
  • Return meaningful error messages for client-side issues
  • Use ExtractError for consistent error handling
  • Validate and sanitize data within the extractor

Custom extractors make your handlers cleaner by moving data extraction and validation logic to reusable components.

⚙️ Configuration

Mint provides flexible configuration options using the functional options pattern for clean, type-safe customization.

Basic Setup
import (
    "log"
    "github.com/cymoo/mint"
)

func main() {
    // Initialize configuration at application startup (recommended)
    m.Initialize(
        m.WithLogger(log.Default()),
        m.WithValidation(true),
    )
    
    // Your application code...
}
Available Options
JSON Encoding/Decoding

Customize JSON marshaling and unmarshaling:

import "encoding/json"

m.Initialize(
    // Custom JSON marshal function
    m.WithJSONMarshal(func(v any) ([]byte, error) {
        return json.MarshalIndent(v, "", "  ") // Pretty print
    }),
    
    // Custom JSON encode function (streaming)
    m.WithJSONEncode(func(w io.Writer, v any) error {
        encoder := json.NewEncoder(w)
        encoder.SetIndent("", "  ")
        return encoder.Encode(v)
    }),
    
    // Custom JSON unmarshal function
    m.WithJSONUnmarshal(json.Unmarshal),
)
Schema Decoder

Customize form and query parameter parsing:

import "github.com/gorilla/schema"

decoder := schema.NewDecoder()
decoder.IgnoreUnknownKeys(true)
decoder.SetAliasTag("form")

m.Initialize(
    m.WithSchemaDecoder(decoder),
)
Logging

Provide a custom logger:

import (
    "log"
    "os"
)

customLogger := log.New(os.Stdout, "[MINT] ", log.LstdFlags)

m.Initialize(
    m.WithLogger(customLogger),
)
Validation

Control validation behavior:

import "github.com/go-playground/validator/v10"

// Disable validation
m.Initialize(
    m.WithValidation(false),
)

// Or use custom validator
v := validator.New()
v.RegisterValidation("customrule", myValidationFunc)

m.Initialize(
    m.WithValidator(v),
)
Error Handling

Customize error response format:

m.Initialize(
    m.WithErrorHandler(func(w http.ResponseWriter, err error) {
        // Custom error logging
        log.Printf("[ERROR] %v", err)
        
        // Custom error response
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(500)
        json.NewEncoder(w).Encode(map[string]string{
            "error": err.Error(),
            "timestamp": time.Now().Format(time.RFC3339),
        })
    }),
)
Configuration Methods
Initialize(opts ...Option)

One-time setup at application startup. Uses sync.Once internally - safe to call multiple times but only the first call takes effect:

func main() {
    m.Initialize(
        m.WithLogger(customLogger),
        m.WithValidation(true),
    )
    
    // Start your server...
}
Configure(opts ...Option)

Runtime configuration updates. Can be called multiple times to modify settings after initialization:

// Enable debug mode at runtime
m.Configure(
    m.WithJSONMarshal(func(v any) ([]byte, error) {
        return json.MarshalIndent(v, "", "  ")
    }),
)
Reset()

Reset to defaults - useful for testing:

func TestSomething(t *testing.T) {
    defer m.Reset() // Restore defaults after test
    
    m.Configure(m.WithValidation(false))
    // Test code...
}
Complete Configuration Example
package main

import (
    "encoding/json"
    "io"
    "log"
    "net/http"
    "os"
    "time"
    
    "github.com/cymoo/mint"
    "github.com/go-playground/validator/v10"
    "github.com/gorilla/schema"
)

func main() {
    // Configure logger
    logger := log.New(os.Stdout, "[API] ", log.LstdFlags|log.Lshortfile)
    
    // Configure schema decoder
    decoder := schema.NewDecoder()
    decoder.IgnoreUnknownKeys(true)
    decoder.SetAliasTag("form")
    
    // Configure validator
    v := validator.New()
    v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
        return len(fl.Field().String()) >= 3
    })
    
    // Initialize framework
    m.Initialize(
        m.WithLogger(logger),
        m.WithSchemaDecoder(decoder),
        m.WithValidator(v),
        m.WithJSONMarshal(func(v any) ([]byte, error) {
            return json.MarshalIndent(v, "", "  ")
        }),
        m.WithErrorHandler(func(w http.ResponseWriter, err error) {
            logger.Printf("Error occurred: %v", err)
            
            response := map[string]any{
                "success": false,
                "error":   err.Error(),
                "time":    time.Now().Unix(),
            }
            
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusInternalServerError)
            json.NewEncoder(w).Encode(response)
        }),
    )
    
    // Setup routes
    mux := http.NewServeMux()
    // ... your routes
    
    logger.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
Thread Safety

All configuration methods are thread-safe:

  • Initialize() uses sync.Once for one-time setup
  • Configure() and Reset() use mutex locks for safe concurrent access
  • Config reads use RWMutex for efficient concurrent access
Default Configuration

If you don't call Initialize() or Configure(), Mint uses sensible defaults:

// Default config (automatically applied)
{
    SchemaDecoder:     schema.NewDecoder() with IgnoreUnknownKeys(true),
    EnableValidation:  true,
    Validator:         validator with JSON/form tag support,
    Logger:            log.Default(),
    JSONMarshalFunc:   json.Marshal,
    JSONUnmarshalFunc: json.Unmarshal,
}

🎯 Complete Example

package main

import (
    "log"
    "net/http"
    "github.com/cymoo/mint"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

var users = map[int]User{
    1: {ID: 1, Name: "Alice", Email: "alice@example.com"},
}

func main() {
    mux := http.NewServeMux()
    
    // List users
    mux.HandleFunc("GET /api/users", m.H(func() []User {
        result := make([]User, 0, len(users))
        for _, u := range users {
            result = append(result, u)
        }
        return result
    }))
    
    // Get user by ID
    mux.HandleFunc("GET /api/users/{id}", m.H(func(id m.Path[int]) (User, error) {
        user, ok := users[id.Value]
        if !ok {
            return User{}, &m.HTTPError{Code: 404, Err: "not_found"}
        }
        return user, nil
    }))
    
    // Create user
    mux.HandleFunc("POST /api/users", m.H(func(body m.JSON[CreateUserRequest]) m.Result[User] {
        user := User{
            ID:    len(users) + 1,
            Name:  body.Value.Name,
            Email: body.Value.Email,
        }
        users[user.ID] = user
        
        return m.Result[User]{
            Code: 201,
            Data: user,
        }
    }))
    
    // Delete user
    mux.HandleFunc("DELETE /api/users/{id}", m.H(func(id m.Path[int]) (m.StatusCode, error) {
        if _, ok := users[id.Value]; !ok {
            return 0, &m.HTTPError{Code: 404, Err: "not_found"}
        }
        delete(users, id.Value)
        return m.StatusCode(204), nil
    }))
    
    log.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

🔍 Error Response Format

Errors are automatically serialized to JSON:

{
  "code": 404,
  "error": "not_found",
  "message": "user not found"
}
Built-in Error Types

The framework handles common errors automatically:

  • json.UnmarshalTypeError → 400 with field details
  • json.SyntaxError → 400 invalid JSON
  • schema.MultiError → 400 with validation messages
  • Generic errors → Status inferred from message (e.g., "not found" → 404)

🎨 Best Practices

1. Use Descriptive Error Messages
return &m.HTTPError{
    Code:    400,
    Err:     "validation_error",
    Message: "email must contain @ symbol",
}
2. Leverage Type Safety
// Good: Type-safe path parameter
func getUser(id m.Path[int]) (User, error)

// Avoid: Manual parsing
func getUser(r *http.Request) (User, error) {
    idStr := r.PathValue("id")
    id, _ := strconv.Atoi(idStr) // Error-prone
}
3. Return Structs for JSON
// Good: Automatic JSON serialization
func listUsers() []User

// Verbose: Manual serialization
func listUsers(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(users)
}
4. Combine Extractors
// Multiple parameters work seamlessly
func updateUser(
    id m.Path[int],
    body m.JSON[UpdateUserRequest],
    q m.Query[Options],
) (User, error) {
    // Handler implementation
}

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📄 License

MIT License - see LICENSE file for details

Documentation

Index

Constants

View Source
const (
	ErrTypeBodyRead       = "body_read_error"
	ErrTypeEmptyBody      = "empty_body"
	ErrTypeFormParse      = "form_parse_error"
	ErrTypePathConversion = "path_conversion_error"
	ErrTypeMissingPath    = "missing_path_value"
	ErrTypeValidation     = "validation_error"
)

Variables

This section is empty.

Functions

func Configure added in v0.4.0

func Configure(opts ...Option)

Configure updates the global configuration (can be called multiple times) Use Initialize for one-time setup, Configure for runtime changes

func H

func H(fn any) http.HandlerFunc

func Initialize added in v0.4.0

func Initialize(opts ...Option)

Initialize sets up the global configuration with options This should be called once at application startup

func NewBodyReadError

func NewBodyReadError(err error) error

func NewEmptyBodyError

func NewEmptyBodyError() error

func NewFormParseError

func NewFormParseError(err error) error

func NewMissingPathError

func NewMissingPathError(field string) error

func NewPathConversionError

func NewPathConversionError(field, value, targetType string, err error) error

func NewValidationError added in v0.3.0

func NewValidationError(err error) error

func Reset added in v0.4.0

func Reset()

Reset resets the configuration to defaults (useful for testing)

func WriteHeaders

func WriteHeaders(w http.ResponseWriter, headers http.Header)

Types

type Config

type Config struct {
	// SchemaDecoder for parsing form and query parameters
	SchemaDecoder *schema.Decoder

	// JSONMarshalFunc for encoding JSON responses
	JSONMarshalFunc func(v any) ([]byte, error)

	// JSONEncodeFunc for streaming JSON encoding
	JSONEncodeFunc func(w io.Writer, v any) error

	// JSONUnmarshalFunc for decoding JSON requests
	JSONUnmarshalFunc func(data []byte, v any) error

	// Logger allows user to provide custom logger
	Logger *log.Logger

	// EnableValidation enables automatic validation for JSON, Query, and Form extractors
	EnableValidation bool

	// Validator is the validation instance to use
	Validator *validator.Validate

	// ErrorHandler allows custom error handling
	ErrorHandler func(w http.ResponseWriter, err error)
}

Config holds global configuration for the framework

type ExtractError

type ExtractError struct {
	Type    string
	Field   string
	Value   string
	Message string
	Err     error
}

func (*ExtractError) Error

func (e *ExtractError) Error() string

func (*ExtractError) Unwrap

func (e *ExtractError) Unwrap() error

type Extractor

type Extractor interface {
	Extract(*http.Request) error
}

type Form

type Form[T any] struct {
	Value T
}

func (*Form[T]) Extract

func (f *Form[T]) Extract(r *http.Request) error

type HTML

type HTML string

type HTTPError

type HTTPError struct {
	Code    int    `json:"code"`
	Err     string `json:"error"`
	Message string `json:"message,omitempty"`
}

func (HTTPError) Error

func (e HTTPError) Error() string

type JSON

type JSON[T any] struct {
	Value T
}

func (*JSON[T]) Extract

func (j *JSON[T]) Extract(r *http.Request) error

type KeySetter

type KeySetter interface {
	SetKey(string)
}

type Option added in v0.4.0

type Option func(*Config)

Option is a functional option for configuring the framework

func WithErrorHandler added in v0.4.0

func WithErrorHandler(handler func(w http.ResponseWriter, err error)) Option

WithErrorHandler sets a custom error handler

func WithJSONEncode added in v0.4.0

func WithJSONEncode(fn func(w io.Writer, v any) error) Option

WithJSONEncode sets a custom JSON encode function

func WithJSONMarshal added in v0.4.0

func WithJSONMarshal(fn func(v any) ([]byte, error)) Option

WithJSONMarshal sets a custom JSON marshal function

func WithJSONUnmarshal added in v0.4.0

func WithJSONUnmarshal(fn func(data []byte, v any) error) Option

WithJSONUnmarshal sets a custom JSON unmarshal function

func WithLogger added in v0.4.0

func WithLogger(logger *log.Logger) Option

WithLogger sets a custom logger

func WithSchemaDecoder added in v0.4.0

func WithSchemaDecoder(decoder *schema.Decoder) Option

WithSchemaDecoder sets a custom schema decoder

func WithValidation added in v0.4.0

func WithValidation(enabled bool) Option

WithValidation enables/disables validation

func WithValidator added in v0.4.0

func WithValidator(v *validator.Validate) Option

WithValidator sets a custom validator

type Path

type Path[T PathValue] struct {
	Value T
	Key   string
}

func (*Path[T]) Extract

func (p *Path[T]) Extract(r *http.Request) error

func (*Path[T]) SetKey

func (p *Path[T]) SetKey(key string)

type PathValue

type PathValue interface {
	~string | ~int | ~int64 | ~uint | ~uint64 | ~float64 | ~bool
}

type Query

type Query[T any] struct {
	Value T
}

func (*Query[T]) Extract

func (q *Query[T]) Extract(r *http.Request) error

type Responder

type Responder interface {
	Respond(w http.ResponseWriter)
}

type ResponseWriter

type ResponseWriter struct {
	http.ResponseWriter
	// contains filtered or unexported fields
}

func (*ResponseWriter) Write

func (rw *ResponseWriter) Write(b []byte) (int, error)

func (*ResponseWriter) WriteHeader

func (rw *ResponseWriter) WriteHeader(code int)

type Result

type Result[T any] struct {
	Code    int
	Headers http.Header

	Data T
	Err  error
}

func Err

func Err[T any](code int, err error) Result[T]

func OK

func OK[T any](data T) Result[T]

type StatusCode

type StatusCode int

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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