A lightweight, type-safe Go web framework built on top of net/http with automatic parameter extraction and elegant response handling.
- π 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
go get github.com/cymoo/mintRequirements: Go 1.23+ (for enhanced routing patterns)
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.
m.H() wraps your handler functions, enabling automatic parameter extraction and response handling:
mux.HandleFunc("POST /users", m.H(handleCreateUser))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=1 β m.Query[Pagination] |
m.Form[T] |
Form data | username=... β m.Form[LoginForm] |
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 |
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
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,
}
}))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)
}))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}
}))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,
}
}))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"))
}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 allow you to extend the framework to handle any type of request data. Here's how to create your own:
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
}- Automatic Injection: The framework automatically calls
Extract()and injects the parsed value - Error Handling: Return
ExtractErrorwith clear type and message for client errors - Type Safety: Leverage Go's type system for validated, type-safe parameters
Simply include your custom extractor as a handler parameter:
func mySecureApi(bearer BearerToken) string {
return "token: " + bearer.Token
}- Keep extractors focused on single responsibility
- Return meaningful error messages for client-side issues
- Use
ExtractErrorfor 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.
Mint provides flexible configuration options using the functional options pattern for clean, type-safe customization.
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...
}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),
)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),
)Provide a custom logger:
import (
"log"
"os"
)
customLogger := log.New(os.Stdout, "[MINT] ", log.LstdFlags)
m.Initialize(
m.WithLogger(customLogger),
)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),
)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),
})
}),
)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...
}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 to defaults - useful for testing:
func TestSomething(t *testing.T) {
defer m.Reset() // Restore defaults after test
m.Configure(m.WithValidation(false))
// Test code...
}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))
}All configuration methods are thread-safe:
Initialize()usessync.Oncefor one-time setupConfigure()andReset()use mutex locks for safe concurrent access- Config reads use
RWMutexfor efficient concurrent access
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,
}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))
}Errors are automatically serialized to JSON:
{
"code": 404,
"error": "not_found",
"message": "user not found"
}The framework handles common errors automatically:
json.UnmarshalTypeErrorβ 400 with field detailsjson.SyntaxErrorβ 400 invalid JSONschema.MultiErrorβ 400 with validation messages- Generic errors β Status inferred from message (e.g., "not found" β 404)
return &m.HTTPError{
Code: 400,
Err: "validation_error",
Message: "email must contain @ symbol",
}// 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
}// Good: Automatic JSON serialization
func listUsers() []User
// Verbose: Manual serialization
func listUsers(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(users)
}// Multiple parameters work seamlessly
func updateUser(
id m.Path[int],
body m.JSON[UpdateUserRequest],
q m.Query[Options],
) (User, error) {
// Handler implementation
}Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details