Pirca is a lightweight HTTP middleware for Go that extends net/http with a rich Context, response helpers, request binders, form/file handling, and more β without replacing the standard library or adding external dependencies.
Built on patterns from Gin, Pirca follows the same conventions and logic, adapted to work directly with net/http. Many methods mirror Gin's API, making it familiar if you've used Gin before, but without the framework lock-in.
- Zero dependencies β only the Go standard library.
- Based on Gin β same patterns, same feel, no framework.
- Full control β
ctx.Requestandctx.Writerare the original*http.Requestandhttp.ResponseWriter. Use them directly whenever you need. - Implements
context.Contextβ passctxdirectly to databases, HTTP clients, tracers, etc. - Captures status code and bytes written β perfect for logging, metrics, and observability middlewares.
- Accelerates development β JSON/XML binders, response writers, file uploads, cookies, query params, form values β all ready to use.
- Go 1.22 or later
go get github.com/loadept/pircapackage main
import (
"log"
"net/http"
"github.com/loadept/pirca"
)
func main() {
mux := http.NewServeMux()
handler := pirca.New()(mux)
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
ctx := pirca.Ctx(r)
_ = ctx.JSON(http.StatusOK, map[string]string{
"message": "Hello world",
})
})
log.Fatal(http.ListenAndServe(":8080", handler))
}pirca.New()returns a middleware that creates aContextper request.- Inside the handler,
pirca.Ctx(r)retrieves theContextfrom the request. - Use
ctxto bind bodies, write responses, handle cookies, query params, forms, files, and more.
Creates the middleware. Must be the outermost layer in your handler chain. Accepts an optional *Config.
// Defaults
handler := pirca.New()(mux)
// With custom config
handler := pirca.New(&pirca.Config{
MaxBodySize: 1 << 20, // 1MB max body
MaxMultipartMemory: 64 << 20, // 64MB for multipart
})(mux)| Field | Type | Default | Description |
|---|---|---|---|
MaxBodySize |
int64 |
0 (no limit) |
Max request body size in bytes |
MaxMultipartMemory |
int64 |
32 MB |
Max memory for multipart forms |
Retrieves the Context from the request. Must be called inside a handler wrapped by New().
ctx := pirca.Ctx(r)Read and deserialize the request body.
| Method | Stream | Cache | Best for |
|---|---|---|---|
BindJSON(obj) |
decoder | β | JSON, single read, efficient |
BindJSONStrict(obj) |
decoder | β | JSON + reject unknown fields |
BindXML(obj) |
decoder | β | XML, single read |
Bind(obj, binder) |
[]byte |
β | Custom formats (TOML, YAML, etc.) |
BindBodyWith(obj, binder) |
[]byte |
β | Custom formats + multiple reads |
BindJSONWith(obj) |
[]byte |
β | JSON + multiple reads |
BindJSONStrictWith(obj) |
[]byte |
β | JSON strict + multiple reads |
BindXMLWith(obj) |
[]byte |
β | XML + multiple reads |
ctx := pirca.Ctx(r)
// Stream JSON (single read)
var user User
if err := ctx.BindJSON(&user); err != nil { ... }
// Strict JSON β rejects unknown fields
if err := ctx.BindJSONStrict(&user); err != nil { ... }
// Custom format (TOML, YAML, MessagePack, etc.)
var cfg Config
if err := ctx.Bind(&cfg, toml.Unmarshal); err != nil { ... }
// Cached body β can be read again by other middlewares
var product Product
if err := ctx.BindBodyWith(&product, json.Unmarshal); err != nil { ... }
ctx.Set("product", product) // share with other handlers
// Convenience cached variants
ctx.BindJSONWith(&user) // json.Unmarshal
ctx.BindJSONStrictWith(&user) // json with DisallowUnknownFields
ctx.BindXMLWith(&doc) // xml.Unmarshal// Read body as bytes (single read)
body, err := ctx.GetBodyBytes()
// Direct reader access
raw, err := io.ReadAll(ctx.Request.Body)Write responses in various formats.
| Method | Content-Type | Description |
|---|---|---|
JSON(code, obj) |
application/json |
Serializes as JSON |
XML(code, obj) |
application/xml |
Serializes as XML |
String(code, msg) |
not set | Plain text |
Data(code, data) |
not set | Raw bytes |
Redirect(code, location) |
β | HTTP redirect |
File(filepath) |
auto | Serves a file |
FileFromFS(filepath, fs) |
auto | Serves from http.FileSystem |
FileAttachment(filepath, filename) |
auto | Forces download |
ctx := pirca.Ctx(r)
ctx.JSON(http.StatusOK, map[string]string{"message": "ok"})
ctx.XML(http.StatusCreated, myStruct)
ctx.String(http.StatusOK, "<h1>Hello</h1>")
ctx.Data(http.StatusOK, pdfBytes)
ctx.Redirect(http.StatusMovedPermanently, "/new-url")
// Files
ctx.File("./static/index.html")
ctx.FileFromFS("static/style.css", http.FS(embedFS))
ctx.FileAttachment("./docs/report.pdf", "report_2026.pdf")Access URL query parameters.
ctx := pirca.Ctx(r)
// GET /search?q=golang&page=1&color=red&color=blue
q := ctx.Query("q") // "golang"
page := ctx.DefaultQuery("page", "1") // "1" (default)
limit := ctx.DefaultQuery("limit", "10") // "10" (not in URL)
value, exists := ctx.GetQuery("q") // ("golang", true)
value, exists := ctx.GetQuery("wtf") // ("", false)
colors := ctx.QueryArray("color") // ["red", "blue"]
values, ok := ctx.GetQueryArray("color") // (["red", "blue"], true)| Method | Returns | Description |
|---|---|---|
Query(key) |
string |
Value or "" |
DefaultQuery(key, default) |
string |
Value or default if missing |
GetQuery(key) |
(string, bool) |
Value + existence check |
QueryArray(key) |
[]string |
All values |
GetQueryArray(key) |
([]string, bool) |
All values + existence |
// Pattern: GET /user/{id}
ctx := pirca.Ctx(r)
id := ctx.Param("id") // "123"Access form fields from application/x-www-form-urlencoded and multipart/form-data.
ctx := pirca.Ctx(r)
name := ctx.FormValue("name") // "jesus" or ""
name := ctx.DefaultFormValue("name", "guest") // "jesus" or "guest" if missing
name, exists := ctx.GetFormValue("name") // ("jesus", true) or ("", false)| Method | Returns | Description |
|---|---|---|
FormValue(key) |
string |
Value or "" |
DefaultFormValue(key, default) |
string |
Value or default if missing |
GetFormValue(key) |
(string, bool) |
Value + existence check |
Handle multipart file uploads.
ctx := pirca.Ctx(r)
// Single file
file, err := ctx.FormFile("avatar")
if err != nil { ... }
ctx.SaveUploadedFile(file, "./uploads/"+file.Filename)
// Multiple files
form, err := ctx.MultipartForm()
if err != nil { ... }
for _, file := range form.File["images"] {
ctx.SaveUploadedFile(file, "./uploads/"+file.Filename)
}| Method | Description |
|---|---|
FormFile(name) |
Returns the first file for the given field |
MultipartForm() |
Returns the full parsed multipart form |
SaveUploadedFile(file, dst, perm...) |
Saves to disk (creates dirs, optional permissions) |
ctx := pirca.Ctx(r)
// Set
ctx.SetSameSite(http.SameSiteLaxMode)
ctx.SetCookie("token", "abc123", 3600, "/", "example.com", true, true)
// Set with pre-built cookie
ctx.SetCookieData(&http.Cookie{
Name: "session",
Value: sessionID,
})
// Get
val, err := ctx.Cookie("token") // http.ErrNoCookie if missing| Method | Description |
|---|---|
SetSameSite(samesite) |
Sets SameSite attribute for subsequent cookies |
SetCookie(name, value, maxAge, path, domain, secure, httpOnly) |
Writes a Set-Cookie header |
SetCookieData(cookie) |
Writes using a pre-built *http.Cookie |
Cookie(name) |
Reads a cookie from the request (URL-decoded) |
ctx := pirca.Ctx(r)
ctx.Header("X-Custom", "value") // set response header
ctx.Header("X-Custom", "") // delete response header
val := ctx.GetHeader("Content-Type") // read request header| Method | Description |
|---|---|
Header(key, value) |
Sets or deletes a response header |
GetHeader(key) |
Returns a request header value |
ctx := pirca.Ctx(r)
ctx.Status(http.StatusCreated)
fmt.Println(ctx.GetStatus()) // 201
fmt.Println(ctx.BytesWritten()) // total bytes written to response body| Method | Description |
|---|---|
Status(code) |
Writes the HTTP status code |
GetStatus() |
Returns the written status code |
BytesWritten() |
Returns total bytes written to the body |
Share data between middlewares and handlers within the same request.
ctx := pirca.Ctx(r)
// Set
ctx.Set("userID", "123")
ctx.Set("role", "admin")
// Get
if userID, ok := ctx.Get("userID"); ok {
fmt.Println(userID)
}
// Delete
ctx.Delete("tempData")All methods are safe for concurrent use.
| Method | Description |
|---|---|
Set(key, value) |
Stores a value (any type) |
Get(key) |
Retrieves a value + exists bool |
Delete(key) |
Removes a value |
*Context implements context.Context, so it can be passed directly to any function that accepts one.
ctx := pirca.Ctx(r)
// Pass to database, HTTP client, tracer, etc.
user, err := db.FindUser(ctx, id)
resp, err := http.NewRequestWithContext(ctx, "GET", url, nil)
span, _ := tracer.Start(ctx, "handler")Deadline()β delegates to the parent request contextDone()β closed when the client disconnectsErr()β returns cancellation errorValue(key)β string keys search the local store first, then fall back to the parent context
Since New() captures the status code and bytes written through an internal responseWriter, you can build middlewares without wrapping the ResponseWriter yourself.
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := pirca.Ctx(r)
start := time.Now()
next.ServeHTTP(w, r)
log.Printf(
"%s %s %d %d %v",
r.Method, r.URL.Path,
ctx.GetStatus(), ctx.BytesWritten(),
time.Since(start),
)
})
}
func main() {
mux := http.NewServeMux()
handler := pirca.New()(loggingMiddleware(mux))
http.ListenAndServe(":8080", handler)
}Because Pirca works directly with net/http, you always have full access to the underlying types:
ctx := pirca.Ctx(r)
// ctx.Request is the original *http.Request
// ctx.Writer is the original http.ResponseWriter (wrapped)
// They are the same references as the handler parameters
fmt.Fprintf(ctx.Writer, "raw write")
ctx.Request.Header.Get("Authorization")
r.Method // also works β r is the same as ctx.RequestYou're never locked into the middleware. Use ctx.Request directly, use ctx.Writer directly, or use the original r and w β they're all the same objects.
func handler(w http.ResponseWriter, r *http.Request) {
ctx := pirca.Ctx(r)
var payload struct {
Name string `json:"name"`
Age int `json:"age"`
}
if err := ctx.BindJSON(&payload); err != nil {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request body",
})
return
}
page := ctx.DefaultQuery("page", "1")
if token, err := ctx.Cookie("session"); err == nil {
ctx.Set("session_token", token)
}
ctx.JSON(http.StatusOK, map[string]any{
"name": payload.Name,
"age": payload.Age,
"page": page,
"agent": ctx.GetHeader("User-Agent"),
})
}MIT