API Specification

API Specification #

This document defines the conventions and standards for building APIs with the INFINI Framework. Following these specifications ensures consistent, predictable, and maintainable API endpoints across all applications built on the framework.

Handler Structure #

Every API handler must embed the base api.Handler struct to gain access to all built-in request and response helpers.

package mymodule

import (
    "infini.sh/framework/core/api"
    httprouter "infini.sh/framework/core/api/router"
    "net/http"
)

type APIHandler struct {
    api.Handler
}

Route Registration #

Routes are registered in the init() function of the module. The framework provides two registration functions:

  • api.HandleUIMethod — registers routes for web UI clients (supports options such as permissions and features)
  • api.HandleAPIMethod — registers low-level API routes without options

Registering Routes #

func init() {
    handler := APIHandler{}

    api.HandleUIMethod(api.GET,    "/resources/",     handler.list,   api.RequirePermission(listPermission))
    api.HandleUIMethod(api.POST,   "/resources/",     handler.create, api.RequirePermission(createPermission))
    api.HandleUIMethod(api.GET,    "/resources/:id",  handler.get,    api.RequirePermission(readPermission))
    api.HandleUIMethod(api.PUT,    "/resources/:id",  handler.update, api.RequirePermission(updatePermission))
    api.HandleUIMethod(api.DELETE, "/resources/:id",  handler.delete, api.RequirePermission(deletePermission))
    api.HandleUIMethod(api.GET,    "/resources/_search", handler.search, api.RequirePermission(searchPermission))
}

HTTP Methods #

ConstantHTTP VerbTypical Use
api.GETGETRetrieve a resource or list
api.POSTPOSTCreate a new resource
api.PUTPUTReplace or update a resource
api.DELETEDELETERemove a resource
api.HEADHEADRetrieve headers only
api.OPTIONSOPTIONSCORS preflight or capability query

Route Patterns #

// Static path
api.HandleUIMethod(api.GET, "/settings", handler.getSettings)

// Required path parameter
api.HandleUIMethod(api.GET, "/resources/:id", handler.get)

// Multiple path parameters
api.HandleUIMethod(api.GET, "/orgs/:orgId/repos/:repoId", handler.getRepo)

// Catch-all parameter — matches everything from this position to the end of the path
api.HandleUIMethod(api.GET, "/files/*filepath", handler.serveFile)

The router supports two parameter types:

SyntaxTypeBehaviour
:nameNamedMatches one path segment (everything up to the next /)
*nameCatch-allMatches from the current position to the end of the path, including nested / characters. The captured value always starts with /. Must appear at the end of the pattern.

For example, with the pattern /files/*filepath:

Request pathfilepath value
/files//
/files/LICENSE/LICENSE
/files/templates/index.html/templates/index.html
/files (no trailing slash)— redirected to /files/

Request Handling #

The handler function signature is fixed for all route handlers:

func (h *APIHandler) methodName(
    w http.ResponseWriter,
    req *http.Request,
    ps httprouter.Params,
) {
    // implementation
}

Path Parameters #

// Named parameter (:id) — panics with 400 if missing
id := ps.MustGetParameter("id")

// Named parameter — returns empty string if absent
id = ps.ByName("id")

// Catch-all parameter (*filepath) — value always starts with "/"
// e.g. for pattern "/files/*filepath" and request "/files/images/logo.png"
// filepath == "/images/logo.png"
filepath := ps.ByName("filepath")

Query Parameters #

// String (returns empty string if absent)
name := h.GetParameter(req, "name")

// String with default
sort := h.GetParameterOrDefault(req, "sort", "created:desc")

// Required string — writes 400 and returns empty if absent
query := h.MustGetParameter(w, req, "q")

// Integer with default
page := h.GetIntOrDefault(req, "page", 1)
size := h.GetIntOrDefault(req, "size", 20)

// Boolean with default
active := h.GetBoolOrDefault(req, "active", true)

Request Headers #

token := h.GetHeader(req, "Authorization", "")
contentType := h.GetHeader(req, "Content-Type", "application/json")

JSON Request Body #

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

// Decode and return error on failure
var body CreateRequest
if err := h.DecodeJSON(req, &body); err != nil {
    h.WriteError(w, "invalid JSON body", http.StatusBadRequest)
    return
}

// Decode and panic on failure (recovery middleware catches it)
h.MustDecodeJSON(req, &body)

Raw Request Body #

data, err := h.GetRawBody(req)
if err != nil {
    h.WriteError(w, err.Error(), http.StatusBadRequest)
    return
}

Response Format #

All API responses use JSON. The framework provides a set of standardized response helpers to keep responses consistent.

Standard JSON Response #

h.WriteJSON(w, payload, http.StatusOK)

The response body can be any JSON-serializable value. For structured payloads, use util.MapStr:

import "infini.sh/framework/core/util"

h.WriteJSON(w, util.MapStr{
    "status":  "healthy",
    "version": "1.0.0",
}, http.StatusOK)

List Response #

Use WriteJSONListResult for paginated collections. The response envelope always includes a total field and a result array:

// Response: {"total": 42, "result": [...]}
h.WriteJSONListResult(w, total, items, http.StatusOK)

CRUD Resource Responses #

The framework provides purpose-built helpers for each CRUD operation. All of them return HTTP 200 and a JSON body with _id and result fields.

Create

h.WriteCreatedOKJSON(w, id)
// {"_id": "<id>", "result": "created"}

Update

h.WriteUpdatedOKJSON(w, id)
// {"_id": "<id>", "result": "updated"}

Delete

h.WriteDeletedOKJSON(w, id)
// {"_id": "<id>", "result": "deleted"}

Get (found)

h.WriteGetOKJSON(w, id, obj)
// {"found": true, "_id": "<id>", "_source": {...}}

Get (not found)

h.WriteGetMissingJSON(w, id)
// {"found": false, "_id": "<id>"}   HTTP 404

Acknowledgement

// Generic acknowledgement with optional extra fields
h.WriteAckJSON(w, true, http.StatusOK, nil)
// {"acknowledged": true}

// Acknowledgement with a message
h.WriteAckWithMessage(w, true, http.StatusOK, "operation completed")
// {"acknowledged": true, "message": "operation completed"}

// Shorthand for acknowledged=true, HTTP 200
h.WriteAckOKJSON(w)

Response Format Reference #

HelperHTTP StatusResponse Body
WriteCreatedOKJSON(w, id)200{"_id":"<id>","result":"created"}
WriteUpdatedOKJSON(w, id)200{"_id":"<id>","result":"updated"}
WriteDeletedOKJSON(w, id)200{"_id":"<id>","result":"deleted"}
WriteGetOKJSON(w, id, obj)200{"found":true,"_id":"<id>","_source":{...}}
WriteGetMissingJSON(w, id)404{"found":false,"_id":"<id>"}
WriteAckOKJSON(w)200{"acknowledged":true}
WriteJSONListResult(w,n,v,s)status{"total":<n>,"result":[...]}
WriteJSON(w, v, status)statusarbitrary JSON

Error Handling #

All error responses use a standard envelope:

{
  "status": 400,
  "error": {
    "reason": "<human-readable message>"
  }
}

Use the following helpers to produce consistent errors:

// 400 Bad Request
h.Error400(w, "missing required field: name")

// 404 Not Found
h.Error404(w)

// 500 Internal Server Error
h.Error500(w, "database connection failed")
h.ErrorInternalServer(w, "unexpected error")

// Custom status code
h.WriteError(w, "conflict detected", http.StatusConflict)

Panic-Based Error Handling #

The framework installs a recovery middleware that intercepts panics and converts them into structured HTTP error responses. You may panic with errors.NewWithHTTPCode to signal specific HTTP status codes:

import "infini.sh/framework/core/errors"

// Panics with a 404; recovery middleware writes the error response
panic(errors.NewWithHTTPCode(404, "resource not found"))

Note: Only use panic-based errors for truly exceptional conditions. Prefer explicit h.WriteError calls in normal validation flows.

Authentication and Authorization #

Require Login #

// User must be authenticated
api.HandleUIMethod(api.GET, "/profile", handler.getProfile,
    api.RequireLogin())

Require Permission #

import "infini.sh/framework/core/security"

var readPermission = security.GetSimplePermission("category", "resource", string(security.Read))

api.HandleUIMethod(api.GET, "/resources/:id", handler.get,
    api.RequirePermission(readPermission))

// Multiple permissions — all must be satisfied
api.HandleUIMethod(api.DELETE, "/admin/resources/:id", handler.delete,
    api.RequirePermission(deletePermission, adminPermission))

Optional Login #

// Proceeds for both authenticated and anonymous users
api.HandleUIMethod(api.GET, "/public/feed", handler.getFeed,
    api.OptionLogin())

Public Access #

// No authentication required
api.HandleUIMethod(api.GET, "/health", handler.healthCheck,
    api.AllowPublicAccess())

Route Options #

Additional route-level options can be applied using functional option arguments.

Feature Flags #

// Enable CORS headers for this endpoint
api.HandleUIMethod(api.GET, "/data", handler.getData,
    api.Feature(core.FeatureCORS))

// Mask sensitive fields in the response
api.HandleUIMethod(api.GET, "/users/:id", handler.getUser,
    api.Feature(core.FeatureMaskSensitiveField))

// Remove sensitive fields entirely from the response
api.HandleUIMethod(api.GET, "/users/:id", handler.getUser,
    api.Feature(core.FeatureRemoveSensitiveField))

// Enable fingerprint-based rate throttling
api.HandleUIMethod(api.POST, "/upload", handler.upload,
    api.Feature(core.FeatureFingerprintThrottle))

Metadata Labels #

Labels provide structured metadata used for logging, auditing, and request tracing:

api.HandleUIMethod(api.POST, "/resources/", handler.create,
    api.Name("Create Resource"),
    api.Resource("resource"),
    api.Action("create"),
    api.Label("operation", "resource_management"),
    api.Tags([]string{"resource", "admin", "create"}))

Complete Example #

The following example demonstrates a complete CRUD API module following all the conventions above.

package resource

import (
    "net/http"

    "infini.sh/framework/core/api"
    httprouter "infini.sh/framework/core/api/router"
    "infini.sh/framework/core/security"
    "infini.sh/framework/core/util"
)

type APIHandler struct {
    api.Handler
}

const category = "myapp"
const resourceType = "resource"

func init() {
    createPermission := security.GetSimplePermission(category, resourceType, string(security.Create))
    readPermission   := security.GetSimplePermission(category, resourceType, string(security.Read))
    updatePermission := security.GetSimplePermission(category, resourceType, string(security.Update))
    deletePermission := security.GetSimplePermission(category, resourceType, string(security.Delete))
    searchPermission := security.GetSimplePermission(category, resourceType, string(security.Search))

    handler := APIHandler{}

    api.HandleUIMethod(api.POST,   "/resources/",        handler.create, api.RequirePermission(createPermission))
    api.HandleUIMethod(api.GET,    "/resources/:id",     handler.get,    api.RequirePermission(readPermission))
    api.HandleUIMethod(api.PUT,    "/resources/:id",     handler.update, api.RequirePermission(updatePermission))
    api.HandleUIMethod(api.DELETE, "/resources/:id",     handler.delete, api.RequirePermission(deletePermission))
    api.HandleUIMethod(api.GET,    "/resources/_search", handler.search, api.RequirePermission(searchPermission))
}

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

func (h *APIHandler) create(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    var obj Resource
    h.MustDecodeJSON(req, &obj)
    obj.ID = util.GetUUID()
    // ... persist obj ...
    h.WriteCreatedOKJSON(w, obj.ID)
}

func (h *APIHandler) get(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    id := ps.MustGetParameter("id")
    obj := Resource{ID: id, Name: "example"}
    // ... load obj by id ...
    h.WriteGetOKJSON(w, id, obj)
}

func (h *APIHandler) update(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    id := ps.MustGetParameter("id")
    var obj Resource
    h.MustDecodeJSON(req, &obj)
    // ... update obj by id ...
    h.WriteUpdatedOKJSON(w, id)
}

func (h *APIHandler) delete(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    id := ps.MustGetParameter("id")
    // ... delete obj by id ...
    h.WriteDeletedOKJSON(w, id)
}

func (h *APIHandler) search(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
    from := h.GetIntOrDefault(req, "from", 0)
    size := h.GetIntOrDefault(req, "size", 10)
    // Replace the lines below with your own data-access logic.
    items := make([]Resource, 0)
    var total int64
    // Example: items, total = store.Query(from, size)
    h.WriteJSONListResult(w, total, items, http.StatusOK)
}
Edit Edit this page