[{"data":1,"prerenderedAt":8604},["ShallowReactive",2],{"search-sections-rocco":3,"nav-rocco":2237,"content-tree-rocco":2298,"footer-resources":2326,"content-/v0.1.21/learn/concepts":5904,"surround-/v0.1.21/learn/concepts":8601},[4,10,15,21,26,32,37,42,47,51,56,61,66,71,76,81,86,91,96,101,106,110,115,120,125,130,135,140,145,149,154,158,163,168,173,178,183,188,193,198,203,208,213,218,223,228,233,238,243,248,252,256,261,265,270,274,278,282,287,292,297,302,307,312,317,322,326,331,336,340,344,349,354,359,363,368,373,378,383,388,392,397,402,407,412,417,421,426,431,436,440,445,450,455,460,465,469,474,479,484,488,493,498,503,507,511,516,521,526,530,535,540,545,550,554,559,564,569,574,579,584,589,593,597,602,607,612,617,621,626,631,636,641,646,650,655,660,665,670,675,680,685,690,695,700,705,710,715,720,725,730,735,740,744,749,752,757,762,767,772,777,781,785,790,795,800,805,810,815,820,825,830,835,839,845,850,855,860,865,870,875,880,884,889,894,899,902,906,910,915,920,923,928,933,938,943,948,952,956,961,965,970,975,980,985,990,995,1000,1004,1009,1014,1018,1023,1028,1032,1037,1042,1047,1051,1056,1061,1066,1069,1074,1079,1084,1088,1093,1098,1103,1107,1112,1117,1122,1126,1131,1136,1140,1145,1150,1155,1160,1165,1170,1175,1180,1183,1188,1193,1198,1203,1207,1212,1217,1222,1225,1230,1235,1240,1245,1250,1255,1259,1263,1268,1273,1278,1282,1287,1292,1297,1302,1307,1312,1317,1321,1326,1331,1336,1341,1346,1351,1356,1361,1366,1371,1375,1379,1383,1388,1393,1398,1403,1407,1412,1417,1422,1427,1432,1436,1440,1445,1450,1454,1459,1464,1469,1472,1477,1482,1487,1492,1497,1502,1506,1511,1516,1521,1526,1531,1536,1541,1546,1550,1555,1559,1564,1568,1572,1577,1580,1585,1589,1594,1599,1604,1609,1614,1619,1624,1629,1634,1639,1644,1649,1654,1659,1662,1667,1672,1676,1681,1686,1691,1696,1701,1706,1711,1716,1721,1726,1731,1735,1739,1744,1749,1754,1759,1763,1768,1773,1778,1783,1788,1793,1798,1802,1807,1812,1816,1821,1826,1831,1836,1841,1845,1850,1855,1860,1865,1870,1874,1879,1883,1888,1892,1896,1901,1905,1910,1914,1917,1922,1927,1932,1937,1942,1947,1952,1957,1962,1967,1972,1977,1981,1985,1990,1994,1999,2004,2009,2013,2018,2022,2027,2031,2036,2041,2046,2051,2055,2060,2064,2069,2074,2079,2083,2088,2093,2098,2103,2108,2112,2117,2122,2127,2132,2137,2142,2147,2152,2156,2161,2166,2170,2175,2180,2185,2189,2194,2198,2203,2208,2213,2218,2223,2228,2233],{"id":5,"title":6,"titles":7,"content":8,"level":9},"/v0.1.21/overview","Overview",[],"Type-safe HTTP framework for Go with automatic OpenAPI generation",1,{"id":11,"title":12,"titles":13,"content":14,"level":9},"/v0.1.21/overview#rocco","Rocco",[],"Type-safe HTTP framework for Go with automatic OpenAPI generation. Rocco provides a declarative, type-safe approach to building HTTP APIs. Define your handlers with generic types, and rocco handles request parsing, error responses, and OpenAPI documentation automatically.",{"id":16,"title":17,"titles":18,"content":19,"level":20},"/v0.1.21/overview#why-rocco","Why Rocco?",[12],"Building HTTP APIs in Go typically involves repetitive boilerplate: parsing request bodies, validating inputs, mapping errors to status codes, and keeping documentation in sync. Rocco eliminates this friction through a type-driven design. ┌─────────────────────────────────────────────────────────────┐\n│                        Your Code                            │\n│  ┌─────────────────────────────────────────────────────┐   │\n│  │   type Input struct { Name string `validate:\"req\"` }│   │\n│  │   type Output struct { ID string }                  │   │\n│  │   func Handle(req) (Output, error) { ... }          │   │\n│  └─────────────────────────────────────────────────────┘   │\n└──────────────────────────┬──────────────────────────────────┘\n                           │\n                           ▼\n┌─────────────────────────────────────────────────────────────┐\n│                        Rocco                                │\n│  ┌────────────┐  ┌────────────┐  ┌────────────────────┐    │\n│  │   Parse    │→ │  Validate  │→ │  Execute Handler   │    │\n│  │   JSON     │  │   Input    │  │  + Marshal Output  │    │\n│  └────────────┘  └────────────┘  └────────────────────┘    │\n│                                                             │\n│  ┌────────────────────────────────────────────────────┐    │\n│  │           OpenAPI Spec Generation                   │    │\n│  │   (schemas, params, errors from your types)        │    │\n│  └────────────────────────────────────────────────────┘    │\n└─────────────────────────────────────────────────────────────┘",2,{"id":22,"title":23,"titles":24,"content":25,"level":20},"/v0.1.21/overview#design-principles","Design Principles",[12],"",{"id":27,"title":28,"titles":29,"content":30,"level":31},"/v0.1.21/overview#type-safety-first","Type Safety First",[12,23],"Every handler declares its input and output types. The compiler catches mismatches, and the framework uses these types to generate accurate OpenAPI schemas. // Compile-time type safety\nhandler := rocco.NewHandler[CreateUserInput, UserOutput](...)\n\n// req.Body is CreateUserInput, not any or interface{}\nfunc(req *rocco.Request[CreateUserInput]) (UserOutput, error) {\n    return UserOutput{Name: req.Body.Name}, nil\n}",3,{"id":33,"title":34,"titles":35,"content":36,"level":31},"/v0.1.21/overview#explicit-over-magic","Explicit Over Magic",[12,23],"No hidden configuration files, no reflection-based routing, no implicit middleware. Every behavior is declared in code. handler.\n    WithPathParams(\"id\").           // Explicit path params\n    WithQueryParams(\"page\", \"limit\"). // Explicit query params\n    WithErrors(ErrNotFound).        // Explicit error declarations\n    WithScopes(\"users:read\")        // Explicit authorization",{"id":38,"title":39,"titles":40,"content":41,"level":31},"/v0.1.21/overview#documentation-as-code","Documentation as Code",[12,23],"OpenAPI schemas are generated from your types. When you change a struct field, the documentation updates automatically. No manual syncing required.",{"id":43,"title":44,"titles":45,"content":46,"level":31},"/v0.1.21/overview#errors-as-values","Errors as Values",[12,23],"Error handling uses sentinel errors with structured responses. Declare which errors a handler may return, and rocco enforces this at runtime while generating accurate error schemas.",{"id":48,"title":49,"titles":50,"content":25,"level":20},"/v0.1.21/overview#core-capabilities","Core Capabilities",[12],{"id":52,"title":53,"titles":54,"content":55,"level":31},"/v0.1.21/overview#typed-handlers","Typed Handlers",[12,49],"Generic handlers with compile-time type checking for request bodies and responses.",{"id":57,"title":58,"titles":59,"content":60,"level":31},"/v0.1.21/overview#type-based-validation","Type-Based Validation",[12,49],"Types that implement the Validatable interface are automatically validated. Use any validation library (like go-playground/validator) or write custom logic. The validate struct tag is parsed for OpenAPI schema generation.",{"id":62,"title":63,"titles":64,"content":65,"level":31},"/v0.1.21/overview#openapi-generation","OpenAPI Generation",[12,49],"Full OpenAPI 3.1.0 specs generated from your code, including request/response schemas, parameters, and error responses.",{"id":67,"title":68,"titles":69,"content":70,"level":31},"/v0.1.21/overview#identity-authorization","Identity & Authorization",[12,49],"Built-in support for identity extraction, scope-based authorization, role-based access control, and usage limits.",{"id":72,"title":73,"titles":74,"content":75,"level":31},"/v0.1.21/overview#structured-errors","Structured Errors",[12,49],"Typed error responses with codes, messages, and optional details. Custom errors with typed detail schemas for comprehensive API contracts.",{"id":77,"title":78,"titles":79,"content":80,"level":31},"/v0.1.21/overview#observability","Observability",[12,49],"Event emission via capitan for request lifecycle, handler execution, and error tracking.",{"id":82,"title":83,"titles":84,"content":85,"level":31},"/v0.1.21/overview#stdlib-router","Stdlib Router",[12,49],"Built on Go 1.22+ stdlib http.ServeMux with native path parameter support. Zero external router dependencies.",{"id":87,"title":88,"titles":89,"content":90,"level":20},"/v0.1.21/overview#quick-example","Quick Example",[12],"package main\n\nimport \"github.com/zoobz-io/rocco\"\n\ntype CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n\ntype UserOutput struct {\n    ID    string `json:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc main() {\n    engine := rocco.NewEngine()\n\n    handler := rocco.NewHandler[CreateUserInput, UserOutput](\n        \"create-user\",\n        \"POST\",\n        \"/users\",\n        func(req *rocco.Request[CreateUserInput]) (UserOutput, error) {\n            return UserOutput{\n                ID:    \"usr_123\",\n                Name:  req.Body.Name,\n                Email: req.Body.Email,\n            }, nil\n        },\n    ).WithSuccessStatus(201)\n\n    engine.WithHandlers(handler)\n    engine.Start(rocco.HostAll, 8080)\n}",{"id":92,"title":93,"titles":94,"content":95,"level":20},"/v0.1.21/overview#next-steps","Next Steps",[12],"Quickstart - Get running in 5 minutesCore Concepts - Understand the building blocksArchitecture - How rocco works internally",{"id":97,"title":98,"titles":99,"content":100,"level":20},"/v0.1.21/overview#see-also","See Also",[12],"Handler Guide - Deep dive into handler configurationError Handling - Error patterns and custom errorsAPI Reference - Complete API documentation html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":102,"title":103,"titles":104,"content":105,"level":9},"/v0.1.21/learn/quickstart","Quickstart",[],"Get started with rocco in 5 minutes",{"id":107,"title":103,"titles":108,"content":109,"level":9},"/v0.1.21/learn/quickstart#quickstart",[],"Get a type-safe HTTP API running in 5 minutes.",{"id":111,"title":112,"titles":113,"content":114,"level":20},"/v0.1.21/learn/quickstart#installation","Installation",[103],"go get github.com/zoobz-io/rocco",{"id":116,"title":117,"titles":118,"content":119,"level":20},"/v0.1.21/learn/quickstart#your-first-api","Your First API",[103],"Create a file main.go: package main\n\nimport (\n    \"fmt\"\n    \"github.com/zoobz-io/rocco\"\n)\n\n// 1. Define your input type (validate tags generate OpenAPI constraints)\ntype CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2,max=100\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n\n// 2. Define your output type\ntype UserOutput struct {\n    ID    string `json:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc main() {\n    // 3. Create an engine\n    engine := rocco.NewEngine()\n\n    // 4. Create a typed handler\n    createUser := rocco.POST[CreateUserInput, UserOutput](\"/users\",\n        func(req *rocco.Request[CreateUserInput]) (UserOutput, error) {\n            // req.Body is your parsed CreateUserInput\n            return UserOutput{\n                ID:    \"usr_\" + req.Body.Name[:3],\n                Name:  req.Body.Name,\n                Email: req.Body.Email,\n            }, nil\n        },\n    ).WithSuccessStatus(201) // Return 201 Created\n\n    // 5. Register handler\n    engine.WithHandlers(createUser)\n\n    // 6. Start server\n    fmt.Println(\"Server running at http://localhost:8080\")\n    engine.Start(rocco.HostAll, 8080)\n} Run it: go run main.go",{"id":121,"title":122,"titles":123,"content":124,"level":20},"/v0.1.21/learn/quickstart#test-your-api","Test Your API",[103],"Create a user: curl -X POST http://localhost:8080/users \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}' Response: {\n  \"id\": \"usr_Joh\",\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\"\n} Try invalid JSON: curl -X POST http://localhost:8080/users \\\n  -H \"Content-Type: application/json\" \\\n  -d '{invalid}' Response (422 Unprocessable Entity): {\n  \"code\": \"UNPROCESSABLE_ENTITY\",\n  \"message\": \"invalid request body\"\n} Note: Runtime validation is opt-in. To validate inputs, implement the Validatable interface on your types. See Concepts: Validation for details.",{"id":126,"title":127,"titles":128,"content":129,"level":20},"/v0.1.21/learn/quickstart#view-openapi-documentation","View OpenAPI Documentation",[103],"Rocco automatically generates OpenAPI documentation. Visit: http://localhost:8080/openapi - OpenAPI JSON spechttp://localhost:8080/docs - Interactive Scalar documentation",{"id":131,"title":132,"titles":133,"content":134,"level":20},"/v0.1.21/learn/quickstart#add-more-handlers","Add More Handlers",[103],"Expand your API with GET, PUT, DELETE handlers: // GET /users/{id}\ngetUser := rocco.GET[rocco.NoBody, UserOutput](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (UserOutput, error) {\n        userID := req.Params.Path[\"id\"]\n        // Fetch user from database...\n        return UserOutput{ID: userID, Name: \"John\", Email: \"john@example.com\"}, nil\n    },\n).WithPathParams(\"id\")\n\n// GET /users (with query params)\ntype UserListOutput struct {\n    Users []UserOutput `json:\"users\"`\n    Total int          `json:\"total\"`\n}\n\nlistUsers := rocco.GET[rocco.NoBody, UserListOutput](\"/users\",\n    func(req *rocco.Request[rocco.NoBody]) (UserListOutput, error) {\n        page := req.Params.Query[\"page\"]\n        limit := req.Params.Query[\"limit\"]\n        // Query database...\n        return UserListOutput{Users: []UserOutput{}, Total: 0}, nil\n    },\n).WithQueryParams(\"page\", \"limit\")\n\n// Register all handlers\nengine.WithHandlers(createUser, getUser, listUsers)",{"id":136,"title":137,"titles":138,"content":139,"level":20},"/v0.1.21/learn/quickstart#handle-errors","Handle Errors",[103],"Use sentinel errors for consistent error responses: getUser := rocco.GET[rocco.NoBody, UserOutput](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (UserOutput, error) {\n        user, err := db.FindUser(req.Params.Path[\"id\"])\n        if err != nil {\n            // Return 404 Not Found\n            return UserOutput{}, rocco.ErrNotFound.WithMessage(\"user not found\")\n        }\n        return UserOutput{...}, nil\n    },\n).\n    WithPathParams(\"id\").\n    WithErrors(rocco.ErrNotFound) // Declare possible errors",{"id":141,"title":142,"titles":143,"content":144,"level":20},"/v0.1.21/learn/quickstart#whats-next","What's Next?",[103],"You've built a type-safe API with automatic OpenAPI documentation. Continue learning: Core Concepts - Understand handlers, requests, and responsesArchitecture - How rocco processes requestsHandler Guide - Advanced handler configuration",{"id":146,"title":98,"titles":147,"content":148,"level":20},"/v0.1.21/learn/quickstart#see-also",[103],"CRUD API Cookbook - Complete CRUD exampleError Handling Guide - Error patternsAPI Reference - Full API documentation html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}",{"id":150,"title":151,"titles":152,"content":153,"level":9},"/v0.1.21/learn/concepts","Core Concepts",[],"Understand handlers, requests, responses, and parameters",{"id":155,"title":151,"titles":156,"content":157,"level":9},"/v0.1.21/learn/concepts#core-concepts",[],"Rocco is built around a few core primitives: Engine, Handler, Request, and Response. Understanding these concepts enables you to build any HTTP API.",{"id":159,"title":160,"titles":161,"content":162,"level":20},"/v0.1.21/learn/concepts#engine","Engine",[151],"The Engine is rocco's HTTP server. It manages handler registration, middleware, and request routing. // Create an engine\nengine := rocco.NewEngine().WithAuthenticator(extractIdentity)\n\n// Add global middleware\nengine.WithMiddleware(loggingMiddleware)\n\n// Register handlers\nengine.WithHandlers(handler1, handler2)\n\n// Start serving (HostAll = \"\", HostLocal = \"localhost\", HostLoopback = \"127.0.0.1\")\nengine.Start(rocco.HostAll, 8080) MethodDescriptionNewEngine()Create engineWithAuthenticator(extractor)Configure identity extraction for authenticationWithMiddleware(mw...)Add global middlewareWithHandlers(handlers...)Register handlersWithModels(models...)Register standalone types for OpenAPI schemasWithSpec(spec)Configure OpenAPI metadataRouter()Access underlying stdlib ServeMuxStart(host, port)Begin serving requestsShutdown(ctx)Graceful shutdown",{"id":164,"title":165,"titles":166,"content":167,"level":20},"/v0.1.21/learn/concepts#handler","Handler",[151],"A Handler is a typed request processor. It declares input/output types and the function that processes requests. handler := rocco.NewHandler[InputType, OutputType](\n    \"handler-name\",  // Unique name for logging/docs\n    \"POST\",          // HTTP method\n    \"/path\",         // URL path\n    func(req *rocco.Request[InputType]) (OutputType, error) {\n        // Process request, return response or error\n        return OutputType{...}, nil\n    },\n)",{"id":169,"title":170,"titles":171,"content":172,"level":31},"/v0.1.21/learn/concepts#handler-configuration","Handler Configuration",[151,165],"Handlers use a builder pattern for configuration: handler := rocco.NewHandler[CreateOrderInput, OrderOutput](\n    \"create-order\",\n    \"POST\",\n    \"/orders\",\n    handleCreateOrder,\n).\n    WithSummary(\"Create a new order\").              // OpenAPI summary\n    WithDescription(\"Creates an order...\").         // OpenAPI description\n    WithTags(\"orders\").                             // OpenAPI tags\n    WithSuccessStatus(201).                         // Success status code\n    WithPathParams(\"id\").                           // Declare path params\n    WithQueryParams(\"include\").                     // Declare query params\n    WithErrors(ErrNotFound, ErrConflict).           // Declare possible errors\n    WithAuthentication().                           // Require authentication\n    WithScopes(\"orders:write\").                     // Require scope\n    WithRoles(\"admin\", \"manager\").                  // Require role\n    WithMaxBodySize(1024 * 1024).                   // 1MB body limit\n    WithMiddleware(customMiddleware)                // Handler-specific middleware",{"id":174,"title":175,"titles":176,"content":177,"level":31},"/v0.1.21/learn/concepts#nobody-type","NoBody Type",[151,165],"For handlers without request bodies (GET, DELETE), use rocco.NoBody: handler := rocco.NewHandler[rocco.NoBody, UserOutput](\n    \"get-user\",\n    \"GET\",\n    \"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (UserOutput, error) {\n        // req.Body is NoBody (empty struct)\n        return UserOutput{...}, nil\n    },\n)",{"id":179,"title":180,"titles":181,"content":182,"level":20},"/v0.1.21/learn/concepts#request","Request",[151],"The Request struct provides access to the parsed body, parameters, identity, and underlying HTTP request. type Request[T any] struct {\n    Context  context.Context    // Request context\n    Request  *http.Request      // Underlying HTTP request\n    Params   *Params            // Path and query parameters\n    Body     T                  // Parsed and validated body\n    Identity Identity           // Authenticated identity (if any)\n}",{"id":184,"title":185,"titles":186,"content":187,"level":31},"/v0.1.21/learn/concepts#accessing-parameters","Accessing Parameters",[151,180],"func(req *rocco.Request[MyInput]) (MyOutput, error) {\n    // Path parameters\n    userID := req.Params.Path[\"id\"]\n\n    // Query parameters\n    page := req.Params.Query[\"page\"]\n    filter := req.Params.Query[\"filter\"]\n\n    // Request body (already parsed and validated)\n    name := req.Body.Name\n\n    // Identity (if authenticated)\n    if req.Identity != nil {\n        tenantID := req.Identity.TenantID()\n    }\n\n    // Underlying request\n    header := req.Request.Header.Get(\"X-Custom\")\n\n    return MyOutput{...}, nil\n}",{"id":189,"title":190,"titles":191,"content":192,"level":31},"/v0.1.21/learn/concepts#parameter-declaration","Parameter Declaration",[151,180],"Handlers must declare their parameters: // Path parameters use {param} syntax\nhandler := rocco.NewHandler[rocco.NoBody, Output](\n    \"get-item\",\n    \"GET\",\n    \"/items/{category}/{id}\",\n    handleGetItem,\n).WithPathParams(\"category\", \"id\")\n\n// Query parameters\nhandler := rocco.NewHandler[rocco.NoBody, Output](\n    \"search\",\n    \"GET\",\n    \"/search\",\n    handleSearch,\n).WithQueryParams(\"q\", \"page\", \"limit\", \"sort\")",{"id":194,"title":195,"titles":196,"content":197,"level":20},"/v0.1.21/learn/concepts#response","Response",[151],"Handlers return a typed output value and an error. Rocco handles serialization automatically.",{"id":199,"title":200,"titles":201,"content":202,"level":31},"/v0.1.21/learn/concepts#success-responses","Success Responses",[151,195],"func(req *rocco.Request[Input]) (Output, error) {\n    // Return output - automatically JSON-encoded\n    return Output{\n        ID:   \"123\",\n        Name: req.Body.Name,\n    }, nil\n} Default success status is 200 OK. Override with WithSuccessStatus(): handler.WithSuccessStatus(201) // 201 Created\nhandler.WithSuccessStatus(204) // 204 No Content",{"id":204,"title":205,"titles":206,"content":207,"level":31},"/v0.1.21/learn/concepts#error-responses","Error Responses",[151,195],"Return errors to send error responses: func(req *rocco.Request[Input]) (Output, error) {\n    user, err := db.FindUser(req.Params.Path[\"id\"])\n    if err != nil {\n        // Sentinel error - returns structured JSON response\n        return Output{}, rocco.ErrNotFound.WithMessage(\"user not found\")\n    }\n    return Output{...}, nil\n} Errors must be declared with WithErrors(): handler.WithErrors(rocco.ErrNotFound, rocco.ErrConflict)",{"id":209,"title":210,"titles":211,"content":212,"level":20},"/v0.1.21/learn/concepts#validation","Validation",[151],"Validation is opt-in via the Validatable interface. Types that implement this interface are automatically validated.",{"id":214,"title":215,"titles":216,"content":217,"level":31},"/v0.1.21/learn/concepts#validatable-interface","Validatable Interface",[151,210],"type Validatable interface {\n    Validate() error\n}",{"id":219,"title":220,"titles":221,"content":222,"level":31},"/v0.1.21/learn/concepts#adding-validation-to-types","Adding Validation to Types",[151,210],"Implement Validate() on your input or output types. Rocco integrates with the check package for clean, composable validation: import \"github.com/zoobz-io/check\"\n\ntype CreateUserInput struct {\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc (c CreateUserInput) Validate() error {\n    return check.All(\n        check.Required(c.Name, \"name\"),\n        check.Email(c.Email, \"email\"),\n    )\n} When a handler uses CreateUserInput, validation runs automatically after JSON parsing. No additional configuration needed.",{"id":224,"title":225,"titles":226,"content":227,"level":31},"/v0.1.21/learn/concepts#using-check-validators","Using check Validators",[151,210],"The check package provides many built-in validators: import \"github.com/zoobz-io/check\"\n\ntype CreateUserInput struct {\n    Name     string `json:\"name\"`\n    Email    string `json:\"email\"`\n    Age      int    `json:\"age\"`\n    Website  string `json:\"website,omitempty\"`\n}\n\nfunc (c CreateUserInput) Validate() error {\n    return check.All(\n        check.Required(c.Name, \"name\"),\n        check.MinLength(c.Name, 2, \"name\"),\n        check.MaxLength(c.Name, 100, \"name\"),\n        check.Required(c.Email, \"email\"),\n        check.Email(c.Email, \"email\"),\n        check.Min(c.Age, 0, \"age\"),\n        check.Max(c.Age, 150, \"age\"),\n        check.URL(c.Website, \"website\"), // optional field, only validated if non-empty\n    )\n}",{"id":229,"title":230,"titles":231,"content":232,"level":31},"/v0.1.21/learn/concepts#output-validation","Output Validation",[151,210],"Output validation is opt-in for performance. Enable with WithOutputValidation(): handler := rocco.POST[Input, Output](\"/path\", fn).\n    WithOutputValidation() // Validates output if Output implements Validatable",{"id":234,"title":235,"titles":236,"content":237,"level":31},"/v0.1.21/learn/concepts#validation-tags-for-openapi","Validation Tags for OpenAPI",[151,210],"The validate struct tag is still parsed for OpenAPI schema generation, even without runtime validation: type CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2,max=100\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n} This generates OpenAPI constraints (minLength, maxLength, format: email) in your spec.",{"id":239,"title":240,"titles":241,"content":242,"level":31},"/v0.1.21/learn/concepts#validation-errors","Validation Errors",[151,210],"Invalid inputs return 422 Unprocessable Entity with detailed field errors: {\n  \"code\": \"VALIDATION_FAILED\",\n  \"message\": \"validation failed\",\n  \"details\": {\n    \"fields\": [\n      {\"field\": \"email\", \"message\": \"must be a valid email address\"}\n    ]\n  }\n}",{"id":244,"title":245,"titles":246,"content":247,"level":20},"/v0.1.21/learn/concepts#identity","Identity",[151],"Identity represents an authenticated user or service. Implement the Identity interface: type Identity interface {\n    ID() string              // Unique identifier\n    TenantID() string        // Tenant/organization ID\n    Scopes() []string        // Permission scopes\n    Roles() []string         // User roles\n    Stats() map[string]int   // Usage statistics\n    HasScope(string) bool    // Check scope\n    HasRole(string) bool     // Check role\n} Configure identity extraction via WithAuthenticator: engine := rocco.NewEngine().WithAuthenticator(func(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    token := r.Header.Get(\"Authorization\")\n    // Validate token, return identity or error\n    return &MyIdentity{...}, nil\n})",{"id":249,"title":93,"titles":250,"content":251,"level":20},"/v0.1.21/learn/concepts#next-steps",[151],"Architecture - How rocco processes requests internallyHandler Guide - Advanced handler patternsError Handling - Error patterns and custom errors",{"id":253,"title":98,"titles":254,"content":255,"level":20},"/v0.1.21/learn/concepts#see-also",[151],"Authentication Guide - Identity and authorizationAPI Reference - Complete API documentation html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":257,"title":258,"titles":259,"content":260,"level":9},"/v0.1.21/learn/architecture","Architecture",[],"How rocco processes requests internally",{"id":262,"title":258,"titles":263,"content":264,"level":9},"/v0.1.21/learn/architecture#architecture",[],"Understanding rocco's internal architecture helps you make better design decisions and debug issues effectively.",{"id":266,"title":267,"titles":268,"content":269,"level":20},"/v0.1.21/learn/architecture#request-flow","Request Flow",[258],"When a request arrives, it flows through several stages: Request\n   │\n   ▼\n┌──────────────────────────────────────────────────────────┐\n│                  stdlib ServeMux                          │\n│  ┌─────────────┐                                         │\n│  │   Route     │  Match path and method to handler       │\n│  │   Matching  │                                         │\n│  └──────┬──────┘                                         │\n└─────────│────────────────────────────────────────────────┘\n          │\n          ▼\n┌──────────────────────────────────────────────────────────┐\n│                 Middleware Chain                          │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │\n│  │   Engine    │→ │   Handler   │→ │    Auth     │      │\n│  │ Middleware  │  │ Middleware  │  │ Middleware  │      │\n│  └─────────────┘  └─────────────┘  └─────────────┘      │\n└─────────────────────────┬────────────────────────────────┘\n                          │\n                          ▼\n┌──────────────────────────────────────────────────────────┐\n│                  Handler Adapter                          │\n│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │\n│  │  Extract    │→ │   Parse     │→ │  Validate   │      │\n│  │  Params     │  │   Body      │  │   Input     │      │\n│  └─────────────┘  └─────────────┘  └─────────────┘      │\n│                          │                               │\n│                          ▼                               │\n│                 ┌─────────────┐                          │\n│                 │   Execute   │  Call handler function   │\n│                 │   Handler   │                          │\n│                 └──────┬──────┘                          │\n│                        │                                 │\n│            ┌───────────┴───────────┐                    │\n│            ▼                       ▼                     │\n│  ┌─────────────────┐    ┌─────────────────┐            │\n│  │ Success Path    │    │  Error Path     │            │\n│  │ Marshal output  │    │ Map to response │            │\n│  │ Write response  │    │ Write error     │            │\n│  └─────────────────┘    └─────────────────┘            │\n└──────────────────────────────────────────────────────────┘\n                          │\n                          ▼\n                      Response",{"id":271,"title":272,"titles":273,"content":25,"level":20},"/v0.1.21/learn/architecture#component-overview","Component Overview",[258],{"id":275,"title":83,"titles":276,"content":277,"level":31},"/v0.1.21/learn/architecture#stdlib-router",[258,272],"Rocco is built on Go 1.22+ stdlib http.ServeMux with native path parameter support. The router handles: URL routing with path parameters (/users/{id})HTTP method routing (GET /users/{id})Context propagation Access the ServeMux directly for advanced use cases: engine.Router().HandleFunc(\"GET /custom\", customHandler)",{"id":279,"title":160,"titles":280,"content":281,"level":31},"/v0.1.21/learn/architecture#engine",[258,272],"The Engine orchestrates the server lifecycle: type Engine struct {\n    config           *EngineConfig\n    server           *http.Server\n    mux              *http.ServeMux\n    globalMiddleware []func(http.Handler) http.Handler\n    handlers         []Endpoint\n    extractIdentity  func(context.Context, *http.Request) (Identity, error)\n    spec             *EngineSpec\n} Key responsibilities: Configure HTTP server timeoutsManage global middlewareRegister handlers with the routerBuild authentication/authorization middlewareGenerate OpenAPI specificationsHandle graceful shutdown",{"id":283,"title":284,"titles":285,"content":286,"level":31},"/v0.1.21/learn/architecture#handler-adapter","Handler Adapter",[258,272],"The handler adapter bridges typed handlers to stdlib's http.Handler interface: func (e *Engine) adaptHandler(handler Endpoint) http.HandlerFunc {\n    return func(w http.ResponseWriter, r *http.Request) {\n        // Emit request received event\n        // Call handler.Process()\n        // Emit completion/failure event\n    }\n}",{"id":288,"title":289,"titles":290,"content":291,"level":31},"/v0.1.21/learn/architecture#handler-processing","Handler Processing",[258,272],"The Handler.Process() method executes the request lifecycle: Extract Parameters - Read path/query params from requestRead Body - Read request body with size limitsParse JSON - Unmarshal body into typed structValidate Input - Run struct validationExecute Handler - Call user functionHandle Errors - Map errors to responsesValidate Output - Optional output validationWrite Response - Marshal and write JSON",{"id":293,"title":294,"titles":295,"content":296,"level":20},"/v0.1.21/learn/architecture#middleware-execution","Middleware Execution",[258],"Middleware executes in a specific order: Request  →  Engine MW 1  →  Engine MW 2  →  Handler MW  →  Auth MW  →  Handler\nResponse ←  Engine MW 1  ←  Engine MW 2  ←  Handler MW  ←  Auth MW  ←  Handler",{"id":298,"title":299,"titles":300,"content":301,"level":31},"/v0.1.21/learn/architecture#engine-middleware","Engine Middleware",[258,294],"Applied to all requests: engine.WithMiddleware(middleware.Logger)\nengine.WithMiddleware(middleware.Recoverer)",{"id":303,"title":304,"titles":305,"content":306,"level":31},"/v0.1.21/learn/architecture#handler-middleware","Handler Middleware",[258,294],"Applied to specific handlers: handler.WithMiddleware(rateLimiter)",{"id":308,"title":309,"titles":310,"content":311,"level":31},"/v0.1.21/learn/architecture#auth-middleware","Auth Middleware",[258,294],"Automatically added when handler requires authentication: handler.WithAuthentication()  // Adds auth middleware\nhandler.WithScopes(\"read\")    // Adds auth + authz middleware\nhandler.WithRoles(\"admin\")    // Adds auth + authz middleware",{"id":313,"title":314,"titles":315,"content":316,"level":20},"/v0.1.21/learn/architecture#error-handling-flow","Error Handling Flow",[258],"Handler returns error\n         │\n         ▼\n    ┌────────────┐\n    │ Is rocco   │──No──→ Log error, return 500\n    │  Error?    │\n    └─────┬──────┘\n          │Yes\n          ▼\n    ┌────────────┐\n    │ Is error   │──No──→ Log warning, return 500\n    │ declared?  │        (undeclared sentinel)\n    └─────┬──────┘\n          │Yes\n          ▼\n    Return structured\n    error response Undeclared sentinel errors indicate a programming error - the handler returned an error it didn't declare. This is logged as a warning and returns 500 to avoid leaking internal details.",{"id":318,"title":319,"titles":320,"content":321,"level":20},"/v0.1.21/learn/architecture#event-emission","Event Emission",[258],"Rocco emits events throughout the request lifecycle via capitan: StageEventsServer lifecycleEngineCreated, EngineStarting, EngineShutdownStarted, EngineShutdownCompleteHandler registrationHandlerRegisteredRequest startRequestReceivedHandler executionHandlerExecuting, HandlerSuccess, HandlerError, HandlerSentinelErrorRequest endRequestCompleted, RequestFailedErrorsRequestParamsInvalid, RequestBodyParseError, RequestValidationInputFailed",{"id":323,"title":63,"titles":324,"content":325,"level":20},"/v0.1.21/learn/architecture#openapi-generation",[258],"OpenAPI specs are generated from handler metadata: Handler Registration\n        │\n        ▼\n┌───────────────────────────────────────┐\n│         Metadata Collection           │\n│  - Input/Output type metadata         │\n│  - Path/Query parameters              │\n│  - Error definitions                  │\n│  - Tags, summary, description         │\n└───────────────────┬───────────────────┘\n                    │\n                    ▼\n┌───────────────────────────────────────┐\n│          Schema Generation            │\n│  - Struct → JSON Schema               │\n│  - Validation tags → constraints      │\n│  - Error details → schemas            │\n└───────────────────┬───────────────────┘\n                    │\n                    ▼\n┌───────────────────────────────────────┐\n│           Spec Assembly               │\n│  - Paths from handlers                │\n│  - Components from types              │\n│  - Info from EngineSpec               │\n└───────────────────────────────────────┘ Type metadata is extracted using sentinel, which scans struct types at handler creation time.",{"id":327,"title":328,"titles":329,"content":330,"level":20},"/v0.1.21/learn/architecture#threading-model","Threading Model",[258],"Handler Registration: All handlers must be registered before calling Start(). Registration is not thread-safe. Request Handling: Each request is handled in its own goroutine. Handlers may run concurrently. Shutdown: Graceful shutdown waits for active requests to complete within the context deadline.",{"id":332,"title":333,"titles":334,"content":335,"level":20},"/v0.1.21/learn/architecture#memory-and-performance","Memory and Performance",[258],"Handler creation: Type metadata is scanned once at handler creationOpenAPI spec: Generated once on first request, cached thereafterBody parsing: Uses io.ReadAll with configurable size limitsValidation: Validator instance created per handler (thread-safe)",{"id":337,"title":93,"titles":338,"content":339,"level":20},"/v0.1.21/learn/architecture#next-steps",[258],"Handler Guide - Advanced handler patternsBest Practices - Production recommendations",{"id":341,"title":98,"titles":342,"content":343,"level":20},"/v0.1.21/learn/architecture#see-also",[258],"Observability Cookbook - Event handling patternsEvents Reference - Complete event list html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}",{"id":345,"title":346,"titles":347,"content":348,"level":9},"/v0.1.21/guides/handlers","Handlers",[],"Deep dive into handler configuration and patterns",{"id":350,"title":351,"titles":352,"content":353,"level":9},"/v0.1.21/guides/handlers#handlers-guide","Handlers Guide",[],"Handlers are the core building block of rocco APIs. This guide covers advanced handler configuration, body parsing, parameters, and common patterns.",{"id":355,"title":356,"titles":357,"content":358,"level":20},"/v0.1.21/guides/handlers#handler-anatomy","Handler Anatomy",[351],"Create handlers using HTTP method shortcuts: handler := rocco.POST[InputType, OutputType](\"/path/{id}\", handlerFunc)\nhandler := rocco.GET[InputType, OutputType](\"/path/{id}\", handlerFunc)\nhandler := rocco.PUT[InputType, OutputType](\"/path/{id}\", handlerFunc)\nhandler := rocco.PATCH[InputType, OutputType](\"/path/{id}\", handlerFunc)\nhandler := rocco.DELETE[InputType, OutputType](\"/path/{id}\", handlerFunc) The generic types [InputType, OutputType] define the request body and response types. Use rocco.NoBody for handlers without request bodies. Handler names are auto-generated from method and path (e.g., GET /users/{id} → get-users-id-a3f1b2c4). Override with WithName() if needed: handler := rocco.GET[rocco.NoBody, User](\"/users/{id}\", getUser).\n    WithName(\"fetch-user-by-id\") // Custom name for logs/OpenAPI operationId",{"id":360,"title":361,"titles":362,"content":25,"level":20},"/v0.1.21/guides/handlers#handler-configuration-methods","Handler Configuration Methods",[351],{"id":364,"title":365,"titles":366,"content":367,"level":31},"/v0.1.21/guides/handlers#documentation","Documentation",[351,361],"handler.\n    WithSummary(\"Create a user\").           // Short summary for OpenAPI\n    WithDescription(\"Long description...\"). // Detailed description\n    WithTags(\"users\", \"admin\")              // OpenAPI tags for grouping",{"id":369,"title":370,"titles":371,"content":372,"level":31},"/v0.1.21/guides/handlers#status-codes","Status Codes",[351,361],"handler.WithSuccessStatus(201) // Override default 200 OK\n\n// Common status codes:\n// 200 OK - Default for most operations\n// 201 Created - Resource creation\n// 204 No Content - Successful with no body",{"id":374,"title":375,"titles":376,"content":377,"level":31},"/v0.1.21/guides/handlers#parameters","Parameters",[351,361],"handler.\n    WithPathParams(\"id\", \"subId\").        // Required path parameters\n    WithQueryParams(\"page\", \"limit\")      // Optional query parameters",{"id":379,"title":380,"titles":381,"content":382,"level":31},"/v0.1.21/guides/handlers#error-declaration","Error Declaration",[351,361],"handler.WithErrors(\n    rocco.ErrNotFound,\n    rocco.ErrConflict,\n    rocco.ErrUnprocessableEntity,\n)",{"id":384,"title":385,"titles":386,"content":387,"level":31},"/v0.1.21/guides/handlers#body-limits","Body Limits",[351,361],"handler.WithMaxBodySize(1 * 1024 * 1024) // 1MB limit (default: 10MB)\nhandler.WithMaxBodySize(0)               // Unlimited (not recommended)",{"id":389,"title":210,"titles":390,"content":391,"level":31},"/v0.1.21/guides/handlers#validation",[351,361],"handler.WithOutputValidation() // Enable output validation (disabled by default)",{"id":393,"title":394,"titles":395,"content":396,"level":31},"/v0.1.21/guides/handlers#response-headers","Response Headers",[351,361],"handler.WithResponseHeaders(map[string]string{\n    \"X-Custom-Header\": \"value\",\n    \"Cache-Control\":   \"no-cache\",\n})",{"id":398,"title":399,"titles":400,"content":401,"level":31},"/v0.1.21/guides/handlers#redirects","Redirects",[351,361],"Return rocco.Redirect to perform HTTP redirects instead of returning a body: handler := rocco.GET[rocco.NoBody, rocco.Redirect](\"/old-path\",\n    func(req *rocco.Request[rocco.NoBody]) (rocco.Redirect, error) {\n        return rocco.Redirect{URL: \"/new-path\"}, nil\n    },\n) Configure the redirect status code (default: 302 Found): // Permanent redirect\nreturn rocco.Redirect{URL: \"/new-path\", Status: http.StatusMovedPermanently} // 301\n\n// See Other (after POST)\nreturn rocco.Redirect{URL: \"/success\", Status: http.StatusSeeOther} // 303\n\n// Temporary redirect (default)\nreturn rocco.Redirect{URL: \"/other\"} // 302 Response headers from WithResponseHeaders() are still applied to redirects (useful for setting cookies): handler := rocco.GET[rocco.NoBody, rocco.Redirect](\"/login\",\n    func(req *rocco.Request[rocco.NoBody]) (rocco.Redirect, error) {\n        return rocco.Redirect{URL: \"/dashboard\"}, nil\n    },\n).WithResponseHeaders(map[string]string{\n    \"Set-Cookie\": \"session=abc123; Path=/; HttpOnly\",\n})",{"id":403,"title":404,"titles":405,"content":406,"level":31},"/v0.1.21/guides/handlers#content-type-codec","Content Type / Codec",[351,361],"// Use a custom codec for serialization\nhandler.WithCodec(xmlCodec) Handlers default to JSON (application/json). Custom codecs allow alternative serialization formats such as XML, YAML, or MessagePack. The codec affects both request body parsing and response serialization. Engine-level defaults apply to all handlers: engine.WithCodec(xmlCodec) // All handlers use XML unless overridden Handler-level overrides take precedence: handler.WithCodec(jsonCodec) // This handler uses JSON despite engine default",{"id":408,"title":409,"titles":410,"content":411,"level":20},"/v0.1.21/guides/handlers#path-parameters","Path Parameters",[351],"Path parameters use curly brace syntax: // Single parameter\nhandler := rocco.GET[rocco.NoBody, User](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (User, error) {\n        id := req.Params.Path[\"id\"] // \"123\"\n        return getUser(id)\n    },\n).WithPathParams(\"id\")\n\n// Multiple parameters\nhandler := rocco.GET[rocco.NoBody, Comment](\"/posts/{postId}/comments/{commentId}\",\n    func(req *rocco.Request[rocco.NoBody]) (Comment, error) {\n        postID := req.Params.Path[\"postId\"]\n        commentID := req.Params.Path[\"commentId\"]\n        return getComment(postID, commentID)\n    },\n).WithPathParams(\"postId\", \"commentId\") Important: Always declare path parameters with WithPathParams(). Undeclared parameters won't cause errors but won't appear in OpenAPI documentation.",{"id":413,"title":414,"titles":415,"content":416,"level":20},"/v0.1.21/guides/handlers#query-parameters","Query Parameters",[351],"Query parameters are optional and accessed via req.Params.Query: handler := rocco.GET[rocco.NoBody, UserList](\"/users\",\n    func(req *rocco.Request[rocco.NoBody]) (UserList, error) {\n        // Get query params (empty string if not provided)\n        page := req.Params.Query[\"page\"]\n        limit := req.Params.Query[\"limit\"]\n        filter := req.Params.Query[\"filter\"]\n\n        // Parse with defaults\n        pageNum := 1\n        if page != \"\" {\n            pageNum, _ = strconv.Atoi(page)\n        }\n\n        return listUsers(pageNum, filter)\n    },\n).WithQueryParams(\"page\", \"limit\", \"filter\") Query parameters must be declared with WithQueryParams() to appear in OpenAPI documentation.",{"id":418,"title":419,"titles":420,"content":25,"level":20},"/v0.1.21/guides/handlers#request-body-handling","Request Body Handling",[351],{"id":422,"title":423,"titles":424,"content":425,"level":31},"/v0.1.21/guides/handlers#typed-bodies","Typed Bodies",[351,419],"For POST, PUT, PATCH requests, define a typed input struct: type CreateOrderInput struct {\n    CustomerID string       `json:\"customer_id\" validate:\"required,uuid\"`\n    Items      []OrderItem  `json:\"items\" validate:\"required,min=1,dive\"`\n    Notes      string       `json:\"notes\" validate:\"max=500\"`\n}\n\nhandler := rocco.POST[CreateOrderInput, Order](\"/orders\",\n    func(req *rocco.Request[CreateOrderInput]) (Order, error) {\n        // req.Body is CreateOrderInput (validated)\n        return createOrder(req.Body.CustomerID, req.Body.Items)\n    },\n)",{"id":427,"title":428,"titles":429,"content":430,"level":31},"/v0.1.21/guides/handlers#empty-bodies","Empty Bodies",[351,419],"For GET, DELETE, or other bodyless requests: handler := rocco.DELETE[rocco.NoBody, DeleteResult](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (DeleteResult, error) {\n        return deleteUser(req.Params.Path[\"id\"])\n    },\n)",{"id":432,"title":433,"titles":434,"content":435,"level":31},"/v0.1.21/guides/handlers#large-bodies","Large Bodies",[351,419],"For file uploads or large payloads, increase the body limit: handler := rocco.POST[UploadInput, UploadResult](\"/upload\", handleUpload).\n    WithMaxBodySize(100 * 1024 * 1024) // 100MB",{"id":437,"title":304,"titles":438,"content":439,"level":20},"/v0.1.21/guides/handlers#handler-middleware",[351],"Add middleware to specific handlers: handler := rocco.POST[Input, Output](\"/action\", handleAction).\n    WithMiddleware(rateLimiter, auditLogger) Middleware executes in order: first added runs first.",{"id":441,"title":442,"titles":443,"content":444,"level":20},"/v0.1.21/guides/handlers#hooks","Hooks",[351],"Hooks provide typed data transformation at handler entry and exit. Unlike middleware, which operates on http.Request/http.ResponseWriter, hooks work directly with the typed In and Out structs. Input types implement Entryable and output types implement Sendable — following the same opt-in interface pattern as Validatable.",{"id":446,"title":447,"titles":448,"content":449,"level":31},"/v0.1.21/guides/handlers#entryable","Entryable",[351,442],"Implement the Entryable interface on an input type to transform it after body parsing and validation, before the handler function: type CreateUserInput struct {\n    Email string `json:\"email\"`\n}\n\nfunc (in *CreateUserInput) OnEntry(ctx context.Context) error {\n    in.Email = strings.ToLower(in.Email)\n    return nil\n} The handler detects the interface automatically: handler := rocco.POST[CreateUserInput, User](\"/users\", createUser)",{"id":451,"title":452,"titles":453,"content":454,"level":31},"/v0.1.21/guides/handlers#sendable","Sendable",[351,442],"Implement the Sendable interface on an output type to transform it after the handler function, before marshalling: type UserList struct {\n    Users []User `json:\"users\"`\n}\n\nfunc (out *UserList) OnSend(ctx context.Context) error {\n    for i := range out.Users {\n        out.Users[i].Email = \"\" // Strip emails from response\n    }\n    return nil\n}",{"id":456,"title":457,"titles":458,"content":459,"level":31},"/v0.1.21/guides/handlers#error-handling","Error Handling",[351,442],"Returning an error from a hook short-circuits processing and returns 500 Internal Server Error: func (in *OrderInput) OnEntry(ctx context.Context) error {\n    if in.TenantID == \"\" {\n        return errors.New(\"tenant ID required\")\n    }\n    return nil\n}",{"id":461,"title":462,"titles":463,"content":464,"level":31},"/v0.1.21/guides/handlers#hooks-vs-middleware","Hooks vs Middleware",[351,442],"HooksMiddlewareOperates onTyped In/Out structshttp.Request/http.ResponseWriterCan modifyInput data, output dataHeaders, context, request flowRunsInside Process(), after parsingOutside Process(), before parsingAccess tocontext.Contexthttp.Request/http.ResponseWriterUse forData transformation, normalisationLogging, CORS, rate limiting",{"id":466,"title":467,"titles":468,"content":25,"level":20},"/v0.1.21/guides/handlers#handler-patterns","Handler Patterns",[351],{"id":470,"title":471,"titles":472,"content":473,"level":31},"/v0.1.21/guides/handlers#resource-handler-pattern","Resource Handler Pattern",[351,467],"Group related handlers by resource: func UserHandlers() []rocco.Endpoint {\n    return []rocco.Endpoint{\n        rocco.GET[rocco.NoBody, UserList](\"/users\", listUsers).\n            WithQueryParams(\"page\", \"limit\"),\n\n        rocco.POST[CreateUserInput, User](\"/users\", createUser).\n            WithSuccessStatus(201),\n\n        rocco.GET[rocco.NoBody, User](\"/users/{id}\", getUser).\n            WithPathParams(\"id\").WithErrors(rocco.ErrNotFound),\n\n        rocco.PUT[UpdateUserInput, User](\"/users/{id}\", updateUser).\n            WithPathParams(\"id\").WithErrors(rocco.ErrNotFound),\n\n        rocco.DELETE[rocco.NoBody, DeleteResult](\"/users/{id}\", deleteUser).\n            WithPathParams(\"id\").WithErrors(rocco.ErrNotFound),\n    }\n}\n\n// Register all at once\nengine.WithHandlers(UserHandlers()...)",{"id":475,"title":476,"titles":477,"content":478,"level":31},"/v0.1.21/guides/handlers#versioned-apis","Versioned APIs",[351,467],"// v1 handlers\nv1Create := rocco.POST[V1CreateInput, V1Output](\"/v1/resources\", handleV1Create).\n    WithTags(\"v1\")\n\n// v2 handlers with different schema\nv2Create := rocco.POST[V2CreateInput, V2Output](\"/v2/resources\", handleV2Create).\n    WithTags(\"v2\")",{"id":480,"title":481,"titles":482,"content":483,"level":31},"/v0.1.21/guides/handlers#conditional-logic","Conditional Logic",[351,467],"handler := rocco.GET[Input, Output](\"/resource/{id}\",\n    func(req *rocco.Request[Input]) (Output, error) {\n        format := req.Params.Query[\"format\"]\n\n        switch format {\n        case \"detailed\":\n            return getDetailedResource(req.Params.Path[\"id\"])\n        case \"summary\":\n            return getSummaryResource(req.Params.Path[\"id\"])\n        default:\n            return getResource(req.Params.Path[\"id\"])\n        }\n    },\n).WithQueryParams(\"format\")",{"id":485,"title":486,"titles":487,"content":25,"level":20},"/v0.1.21/guides/handlers#common-mistakes","Common Mistakes",[351],{"id":489,"title":490,"titles":491,"content":492,"level":31},"/v0.1.21/guides/handlers#forgetting-parameter-declaration","Forgetting Parameter Declaration",[351,486],"// WRONG - param won't appear in OpenAPI\nhandler := rocco.GET[rocco.NoBody, User](\"/users/{id}\", getUser)\n\n// CORRECT\nhandler := rocco.GET[rocco.NoBody, User](\"/users/{id}\", getUser).\n    WithPathParams(\"id\")",{"id":494,"title":495,"titles":496,"content":497,"level":31},"/v0.1.21/guides/handlers#undeclared-errors","Undeclared Errors",[351,486],"// WRONG - returns 500 instead of 404\nhandler := rocco.GET[rocco.NoBody, User](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (User, error) {\n        return User{}, rocco.ErrNotFound // Undeclared!\n    },\n)\n\n// CORRECT\nhandler := rocco.GET[rocco.NoBody, User](\"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (User, error) {\n        return User{}, rocco.ErrNotFound\n    },\n).WithErrors(rocco.ErrNotFound)",{"id":499,"title":500,"titles":501,"content":502,"level":31},"/v0.1.21/guides/handlers#wrong-nobody-usage","Wrong NoBody Usage",[351,486],"// WRONG - NoBody for POST with body\nhandler := rocco.POST[rocco.NoBody, User](\"/users\", createUser)\n\n// CORRECT - use typed input\nhandler := rocco.POST[CreateUserInput, User](\"/users\", createUser)",{"id":504,"title":98,"titles":505,"content":506,"level":20},"/v0.1.21/guides/handlers#see-also",[351],"Error Handling - Error patterns and custom errorsAuthentication - Identity and authorizationAPI Reference - Complete API documentation html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}",{"id":508,"title":457,"titles":509,"content":510,"level":9},"/v0.1.21/guides/errors",[],"Error patterns, sentinel errors, and custom error types",{"id":512,"title":513,"titles":514,"content":515,"level":9},"/v0.1.21/guides/errors#error-handling-guide","Error Handling Guide",[],"Rocco uses typed sentinel errors for consistent, documented error responses. This guide covers error patterns, custom errors, and best practices.",{"id":517,"title":518,"titles":519,"content":520,"level":20},"/v0.1.21/guides/errors#error-response-format","Error Response Format",[513],"All errors return a consistent JSON structure: {\n  \"code\": \"NOT_FOUND\",\n  \"message\": \"user not found\",\n  \"details\": {\n    \"resource\": \"user\"\n  }\n} FieldDescriptioncodeMachine-readable error code (e.g., NOT_FOUND)messageHuman-readable messagedetailsOptional structured details (varies by error type)",{"id":522,"title":523,"titles":524,"content":525,"level":20},"/v0.1.21/guides/errors#built-in-errors","Built-in Errors",[513],"Rocco provides typed errors for common HTTP status codes: ErrorStatusCodeDetails TypeErrBadRequest400BAD_REQUESTBadRequestDetailsErrUnauthorized401UNAUTHORIZEDUnauthorizedDetailsErrForbidden403FORBIDDENForbiddenDetailsErrNotFound404NOT_FOUNDNotFoundDetailsErrConflict409CONFLICTConflictDetailsErrPayloadTooLarge413PAYLOAD_TOO_LARGEPayloadTooLargeDetailsErrUnprocessableEntity422UNPROCESSABLE_ENTITYUnprocessableEntityDetailsErrValidationFailed422VALIDATION_FAILEDValidationDetailsErrTooManyRequests429TOO_MANY_REQUESTSTooManyRequestsDetailsErrInternalServer500INTERNAL_SERVER_ERRORInternalServerDetailsErrNotImplemented501NOT_IMPLEMENTEDNotImplementedDetailsErrServiceUnavailable503SERVICE_UNAVAILABLEServiceUnavailableDetails",{"id":527,"title":528,"titles":529,"content":25,"level":20},"/v0.1.21/guides/errors#using-errors","Using Errors",[513],{"id":531,"title":532,"titles":533,"content":534,"level":31},"/v0.1.21/guides/errors#simple-error-return","Simple Error Return",[513,528],"func(req *rocco.Request[rocco.NoBody]) (User, error) {\n    user, err := db.FindUser(req.Params.Path[\"id\"])\n    if err != nil {\n        return User{}, rocco.ErrNotFound\n    }\n    return user, nil\n}",{"id":536,"title":537,"titles":538,"content":539,"level":31},"/v0.1.21/guides/errors#with-custom-message","With Custom Message",[513,528],"return User{}, rocco.ErrNotFound.WithMessage(\"user not found\")",{"id":541,"title":542,"titles":543,"content":544,"level":31},"/v0.1.21/guides/errors#with-typed-details","With Typed Details",[513,528],"return User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n    Resource: \"user\",\n}) Response: {\n  \"code\": \"NOT_FOUND\",\n  \"message\": \"not found\",\n  \"details\": {\n    \"resource\": \"user\"\n  }\n}",{"id":546,"title":547,"titles":548,"content":549,"level":31},"/v0.1.21/guides/errors#combining-message-and-details","Combining Message and Details",[513,528],"return User{}, rocco.ErrNotFound.\n    WithMessage(\"user not found\").\n    WithDetails(rocco.NotFoundDetails{Resource: \"user\"})",{"id":551,"title":380,"titles":552,"content":553,"level":20},"/v0.1.21/guides/errors#error-declaration",[513],"All sentinel errors must be declared with WithErrors(): handler := rocco.NewHandler[rocco.NoBody, User](\n    \"get-user\",\n    \"GET\",\n    \"/users/{id}\",\n    getUser,\n).WithErrors(rocco.ErrNotFound, rocco.ErrForbidden)",{"id":555,"title":556,"titles":557,"content":558,"level":31},"/v0.1.21/guides/errors#why-declaration-matters","Why Declaration Matters",[513,380],"OpenAPI Generation: Declared errors appear in the API spec with proper schemasRuntime Safety: Undeclared errors return 500 to prevent information leakageDocumentation: Consumers know which errors to expect",{"id":560,"title":561,"titles":562,"content":563,"level":31},"/v0.1.21/guides/errors#undeclared-error-behavior","Undeclared Error Behavior",[513,380],"If a handler returns an undeclared sentinel error: Rocco logs a warning with the error detailsReturns 500 Internal Server Error to the clientThe original error code is hidden This catches programming errors where handlers return errors they didn't declare.",{"id":565,"title":566,"titles":567,"content":568,"level":20},"/v0.1.21/guides/errors#custom-errors","Custom Errors",[513],"Define domain-specific errors with typed details:",{"id":570,"title":571,"titles":572,"content":573,"level":31},"/v0.1.21/guides/errors#step-1-define-details-type","Step 1: Define Details Type",[513,566],"type InsufficientFundsDetails struct {\n    Required  float64 `json:\"required\" description:\"Amount required\"`\n    Available float64 `json:\"available\" description:\"Amount available\"`\n    Currency  string  `json:\"currency\" description:\"Currency code\"`\n}",{"id":575,"title":576,"titles":577,"content":578,"level":31},"/v0.1.21/guides/errors#step-2-create-error","Step 2: Create Error",[513,566],"var ErrInsufficientFunds = rocco.NewError[InsufficientFundsDetails](\n    \"INSUFFICIENT_FUNDS\",  // Code\n    402,                   // Status\n    \"insufficient funds\",  // Default message\n)",{"id":580,"title":581,"titles":582,"content":583,"level":31},"/v0.1.21/guides/errors#step-3-use-in-handler","Step 3: Use in Handler",[513,566],"handler := rocco.NewHandler[TransferInput, TransferResult](\n    \"transfer-funds\",\n    \"POST\",\n    \"/transfers\",\n    func(req *rocco.Request[TransferInput]) (TransferResult, error) {\n        balance := getBalance(req.Body.FromAccount)\n        if balance \u003C req.Body.Amount {\n            return TransferResult{}, ErrInsufficientFunds.WithDetails(InsufficientFundsDetails{\n                Required:  req.Body.Amount,\n                Available: balance,\n                Currency:  \"USD\",\n            })\n        }\n        return executeTransfer(req.Body)\n    },\n).WithErrors(ErrInsufficientFunds, rocco.ErrNotFound) Response: {\n  \"code\": \"INSUFFICIENT_FUNDS\",\n  \"message\": \"insufficient funds\",\n  \"details\": {\n    \"required\": 100.00,\n    \"available\": 50.00,\n    \"currency\": \"USD\"\n  }\n}",{"id":585,"title":586,"titles":587,"content":588,"level":20},"/v0.1.21/guides/errors#error-wrapping","Error Wrapping",[513],"Wrap underlying errors for debugging: return User{}, rocco.ErrInternalServer.WithCause(err) The cause is logged but not exposed to clients.",{"id":590,"title":240,"titles":591,"content":592,"level":20},"/v0.1.21/guides/errors#validation-errors",[513],"Validation failures automatically return structured errors: {\n  \"code\": \"VALIDATION_FAILED\",\n  \"message\": \"validation failed\",\n  \"details\": {\n    \"fields\": [\n      {\"field\": \"Email\", \"tag\": \"email\", \"value\": \"not-an-email\"},\n      {\"field\": \"Name\", \"tag\": \"min\", \"value\": \"A\"}\n    ]\n  }\n} These are generated automatically from validate struct tags.",{"id":594,"title":595,"titles":596,"content":25,"level":20},"/v0.1.21/guides/errors#error-patterns","Error Patterns",[513],{"id":598,"title":599,"titles":600,"content":601,"level":31},"/v0.1.21/guides/errors#resource-not-found","Resource Not Found",[513,595],"user, err := db.FindUser(id)\nif errors.Is(err, sql.ErrNoRows) {\n    return User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n        Resource: \"user\",\n    })\n}\nif err != nil {\n    return User{}, rocco.ErrInternalServer.WithCause(err)\n}",{"id":603,"title":604,"titles":605,"content":606,"level":31},"/v0.1.21/guides/errors#conflict-detection","Conflict Detection",[513,595],"_, err := db.CreateUser(input)\nif isUniqueViolation(err) {\n    return User{}, rocco.ErrConflict.\n        WithMessage(\"email already registered\").\n        WithDetails(rocco.ConflictDetails{\n            Reason: \"a user with this email already exists\",\n        })\n}",{"id":608,"title":609,"titles":610,"content":611,"level":31},"/v0.1.21/guides/errors#authorization-errors","Authorization Errors",[513,595],"if !req.Identity.HasScope(\"users:write\") {\n    return User{}, rocco.ErrForbidden.WithMessage(\"requires users:write scope\")\n}",{"id":613,"title":614,"titles":615,"content":616,"level":31},"/v0.1.21/guides/errors#multiple-error-conditions","Multiple Error Conditions",[513,595],"func(req *rocco.Request[UpdateUserInput]) (User, error) {\n    user, err := db.FindUser(req.Params.Path[\"id\"])\n    if errors.Is(err, sql.ErrNoRows) {\n        return User{}, rocco.ErrNotFound\n    }\n    if err != nil {\n        return User{}, rocco.ErrInternalServer.WithCause(err)\n    }\n\n    if user.TenantID != req.Identity.TenantID() {\n        return User{}, rocco.ErrForbidden.WithMessage(\"cannot access other tenant's users\")\n    }\n\n    updated, err := db.UpdateUser(user.ID, req.Body)\n    if isUniqueViolation(err) {\n        return User{}, rocco.ErrConflict.\n            WithMessage(\"email already in use\").\n            WithDetails(rocco.ConflictDetails{\n                Reason: \"this email is already registered to another user\",\n            })\n    }\n\n    return updated, nil\n}",{"id":618,"title":619,"titles":620,"content":25,"level":20},"/v0.1.21/guides/errors#best-practices","Best Practices",[513],{"id":622,"title":623,"titles":624,"content":625,"level":31},"/v0.1.21/guides/errors#_1-declare-all-errors","1. Declare All Errors",[513,619],"Always declare every error a handler may return: handler.WithErrors(\n    rocco.ErrNotFound,\n    rocco.ErrConflict,\n    rocco.ErrForbidden,\n)",{"id":627,"title":628,"titles":629,"content":630,"level":31},"/v0.1.21/guides/errors#_2-use-specific-error-types","2. Use Specific Error Types",[513,619],"// GOOD - specific error\nreturn User{}, rocco.ErrNotFound\n\n// AVOID - generic error\nreturn User{}, errors.New(\"not found\")",{"id":632,"title":633,"titles":634,"content":635,"level":31},"/v0.1.21/guides/errors#_3-add-context-with-details","3. Add Context with Details",[513,619],"// GOOD - useful context\nreturn User{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n    Resource: \"user\",\n})\n\n// LESS USEFUL - no context\nreturn User{}, rocco.ErrNotFound",{"id":637,"title":638,"titles":639,"content":640,"level":31},"/v0.1.21/guides/errors#_4-hide-internal-errors","4. Hide Internal Errors",[513,619],"// GOOD - hide internal details\nif err != nil {\n    return User{}, rocco.ErrInternalServer.WithCause(err)\n}\n\n// BAD - exposes internal error\nreturn User{}, err",{"id":642,"title":643,"titles":644,"content":645,"level":31},"/v0.1.21/guides/errors#_5-domain-specific-errors","5. Domain-Specific Errors",[513,619],"Create custom errors for domain concepts: var (\n    ErrOrderCancelled = rocco.NewError[OrderCancelledDetails](\n        \"ORDER_CANCELLED\", 400, \"order already cancelled\")\n    ErrInventoryLow = rocco.NewError[InventoryDetails](\n        \"INVENTORY_LOW\", 409, \"insufficient inventory\")\n)",{"id":647,"title":98,"titles":648,"content":649,"level":20},"/v0.1.21/guides/errors#see-also",[513],"API Reference - Errors - Complete error referenceHandler Guide - Handler configurationBest Practices - Production patterns html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":651,"title":652,"titles":653,"content":654,"level":9},"/v0.1.21/guides/authentication","Authentication & Authorization",[],"Identity extraction, scopes, roles, and usage limits",{"id":656,"title":657,"titles":658,"content":659,"level":9},"/v0.1.21/guides/authentication#authentication-authorization-guide","Authentication & Authorization Guide",[],"Rocco provides built-in support for identity extraction, scope-based authorization, role-based access control, and usage limits.",{"id":661,"title":662,"titles":663,"content":664,"level":20},"/v0.1.21/guides/authentication#identity-interface","Identity Interface",[657],"Implement the Identity interface to represent authenticated users: type Identity interface {\n    ID() string              // Unique user/service identifier\n    TenantID() string        // Tenant/organization ID (multi-tenancy)\n    Email() string           // Email address\n    Scopes() []string        // All granted scopes\n    Roles() []string         // All assigned roles\n    HasScope(string) bool    // Check if identity has scope\n    HasRole(string) bool     // Check if identity has role\n    Stats() map[string]int   // Usage statistics for rate limiting\n}",{"id":666,"title":667,"titles":668,"content":669,"level":31},"/v0.1.21/guides/authentication#example-implementation","Example Implementation",[657,662],"type UserIdentity struct {\n    id       string\n    tenantID string\n    email    string\n    scopes   []string\n    roles    []string\n    stats    map[string]int\n}\n\nfunc (u *UserIdentity) ID() string            { return u.id }\nfunc (u *UserIdentity) TenantID() string      { return u.tenantID }\nfunc (u *UserIdentity) Email() string         { return u.email }\nfunc (u *UserIdentity) Scopes() []string      { return u.scopes }\nfunc (u *UserIdentity) Roles() []string       { return u.roles }\nfunc (u *UserIdentity) Stats() map[string]int { return u.stats }\n\nfunc (u *UserIdentity) HasScope(scope string) bool {\n    for _, s := range u.scopes {\n        if s == scope {\n            return true\n        }\n    }\n    return false\n}\n\nfunc (u *UserIdentity) HasRole(role string) bool {\n    for _, r := range u.roles {\n        if r == role {\n            return true\n        }\n    }\n    return false\n}",{"id":671,"title":672,"titles":673,"content":674,"level":20},"/v0.1.21/guides/authentication#identity-extraction","Identity Extraction",[657],"Provide an identity extractor when creating the engine: engine := rocco.NewEngine().WithAuthenticator(extractIdentity)\n\nfunc extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // Get token from header\n    token := r.Header.Get(\"Authorization\")\n    if token == \"\" {\n        return nil, errors.New(\"missing authorization header\")\n    }\n\n    // Validate token (JWT, session lookup, API key, etc.)\n    claims, err := validateToken(strings.TrimPrefix(token, \"Bearer \"))\n    if err != nil {\n        return nil, err\n    }\n\n    // Return identity\n    return &UserIdentity{\n        id:       claims.Subject,\n        tenantID: claims.TenantID,\n        scopes:   claims.Scopes,\n        roles:    claims.Roles,\n        stats:    fetchStats(claims.Subject),\n    }, nil\n}",{"id":676,"title":677,"titles":678,"content":679,"level":31},"/v0.1.21/guides/authentication#jwt-example","JWT Example",[657,672],"func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    auth := r.Header.Get(\"Authorization\")\n    if !strings.HasPrefix(auth, \"Bearer \") {\n        return nil, errors.New(\"invalid authorization header\")\n    }\n\n    tokenString := strings.TrimPrefix(auth, \"Bearer \")\n\n    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n        return []byte(secretKey), nil\n    })\n    if err != nil {\n        return nil, err\n    }\n\n    claims, ok := token.Claims.(jwt.MapClaims)\n    if !ok || !token.Valid {\n        return nil, errors.New(\"invalid token\")\n    }\n\n    return &UserIdentity{\n        id:       claims[\"sub\"].(string),\n        tenantID: claims[\"tenant_id\"].(string),\n        scopes:   parseScopes(claims[\"scope\"]),\n        roles:    parseRoles(claims[\"roles\"]),\n    }, nil\n}",{"id":681,"title":682,"titles":683,"content":684,"level":31},"/v0.1.21/guides/authentication#api-key-example","API Key Example",[657,672],"func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    apiKey := r.Header.Get(\"X-API-Key\")\n    if apiKey == \"\" {\n        return nil, errors.New(\"missing API key\")\n    }\n\n    // Look up API key in database\n    keyInfo, err := db.GetAPIKey(apiKey)\n    if err != nil {\n        return nil, errors.New(\"invalid API key\")\n    }\n\n    return &ServiceIdentity{\n        id:       keyInfo.ServiceID,\n        tenantID: keyInfo.TenantID,\n        scopes:   keyInfo.Scopes,\n    }, nil\n}",{"id":686,"title":687,"titles":688,"content":689,"level":31},"/v0.1.21/guides/authentication#auth0-integration","Auth0 Integration",[657,672],"For Auth0-based authentication, use the rocco/auth0 package: import \"github.com/zoobz-io/rocco/auth0\"\n\nextractor, err := auth0.NewExtractor(auth0.Config{\n    Domain:   \"your-tenant.auth0.com\",\n    Audience: \"https://your-api.example.com\",\n})\nif err != nil {\n    log.Fatal(err)\n}\n\nengine := rocco.NewEngine().WithAuthenticator(extractor) The auth0 package handles: JWKS fetching and cachingRS256 signature validationIssuer and audience validationToken expiration checksClaim extraction to rocco.Identity For custom claim paths (Auth0 namespaced claims): auth0.Config{\n    Domain:      \"your-tenant.auth0.com\",\n    Audience:    \"https://your-api.example.com\",\n    RolesClaim:  \"https://myapp.com/roles\",   // Custom roles claim\n    TenantClaim: \"https://myapp.com/org_id\",  // Multi-tenancy claim\n} See the Authentication Cookbook for complete Auth0 examples including user enrichment patterns.",{"id":691,"title":692,"titles":693,"content":694,"level":31},"/v0.1.21/guides/authentication#session-based-oauth","Session-Based OAuth",[657,672],"The rocco/session package provides cookie-based session management with built-in OAuth support. The rocco/oauth package handles the protocol layer (authorization URLs, token exchange, refresh). import (\n    \"github.com/zoobz-io/rocco/oauth\"\n    \"github.com/zoobz-io/rocco/session\"\n)\n\nstore := session.NewMemoryStore() // Use Redis/database in production\n\ncfg := session.Config{\n    OAuth: oauth.GitHub(),\n    Store: store,\n    Cookie: session.CookieConfig{\n        SignKey: []byte(os.Getenv(\"SESSION_KEY\")),\n    },\n    Resolve: func(ctx context.Context, tokens *oauth.TokenResponse) (*session.Data, error) {\n        // Call provider API to get user info, build session data.\n        user, err := fetchUser(ctx, tokens.AccessToken)\n        if err != nil {\n            return nil, err\n        }\n        return &session.Data{\n            UserID: user.ID,\n            Email:  user.Email,\n            Roles:  user.Roles,\n        }, nil\n    },\n    RedirectURL: \"/dashboard\",\n}\ncfg.OAuth.ClientID = os.Getenv(\"GITHUB_CLIENT_ID\")\ncfg.OAuth.ClientSecret = os.Getenv(\"GITHUB_CLIENT_SECRET\")\ncfg.OAuth.RedirectURI = \"https://myapp.com/auth/callback\"\ncfg.OAuth.Scopes = []string{\"read:user\", \"read:org\"}\n\nlogin, _ := session.NewLoginHandler(\"/auth/login\", cfg)\ncallback, _ := session.NewCallbackHandler(\"/auth/callback\", cfg)\nlogout, _ := session.NewLogoutHandler(\"/auth/logout\", cfg, \"/\")\n\nengine := rocco.NewEngine()\nengine.WithAuthenticator(session.Extractor(store, cfg.Cookie))\nengine.WithHandlers(login, callback, logout) The session package handles: CSRF state generation and verification (single-use)Token exchange via rocco/oauthSession creation with signed cookies (HMAC-SHA256)Identity extraction from session cookiesLogout with session deletion and cookie clearing The Resolve function is where the application maps tokens to session data — call the provider's user-info API, look up roles in a database, etc. The oauth package is provider-agnostic. Use oauth.GitHub() for GitHub, oauth.GitHubEnterprise(baseURL) for GHE, or configure custom endpoints for any OAuth 2.0 provider. For token refresh: newTokens, err := oauth.Refresh(ctx, cfg.OAuth, oldRefreshToken)\nif err != nil {\n    // Refresh failed - user must re-authenticate\n} See the Authentication Cookbook for complete examples.",{"id":696,"title":697,"titles":698,"content":699,"level":20},"/v0.1.21/guides/authentication#requiring-authentication","Requiring Authentication",[657],"Mark handlers as requiring authentication: handler := rocco.NewHandler[rocco.NoBody, User](\n    \"get-profile\",\n    \"GET\",\n    \"/profile\",\n    func(req *rocco.Request[rocco.NoBody]) (User, error) {\n        // req.Identity is guaranteed to be non-nil\n        return getUser(req.Identity.ID())\n    },\n).WithAuthentication() Unauthenticated requests receive 401 Unauthorized.",{"id":701,"title":702,"titles":703,"content":704,"level":20},"/v0.1.21/guides/authentication#scope-based-authorization","Scope-Based Authorization",[657],"Require specific scopes: // Single scope required\nhandler.WithScopes(\"users:read\")\n\n// Any of these scopes (OR logic)\nhandler.WithScopes(\"users:read\", \"users:admin\")\n\n// Multiple scope groups (AND logic)\nhandler.WithScopes(\"users:read\").WithScopes(\"verified\")\n// Requires (users:read OR users:admin) AND verified",{"id":706,"title":707,"titles":708,"content":709,"level":31},"/v0.1.21/guides/authentication#scope-patterns","Scope Patterns",[657,702],"Common scope naming conventions: // Resource-based\nhandler.WithScopes(\"users:read\")\nhandler.WithScopes(\"users:write\")\nhandler.WithScopes(\"orders:read\", \"orders:write\")\n\n// Action-based\nhandler.WithScopes(\"read\")\nhandler.WithScopes(\"write\")\nhandler.WithScopes(\"delete\")\n\n// Feature-based\nhandler.WithScopes(\"analytics\")\nhandler.WithScopes(\"reports\")\nhandler.WithScopes(\"admin\")",{"id":711,"title":712,"titles":713,"content":714,"level":20},"/v0.1.21/guides/authentication#role-based-access-control","Role-Based Access Control",[657],"Require specific roles: // Single role required\nhandler.WithRoles(\"admin\")\n\n// Any of these roles (OR logic)\nhandler.WithRoles(\"admin\", \"moderator\")\n\n// Multiple role groups (AND logic)\nhandler.WithRoles(\"admin\").WithRoles(\"verified\")\n// Requires (admin OR moderator) AND verified",{"id":716,"title":717,"titles":718,"content":719,"level":31},"/v0.1.21/guides/authentication#combining-scopes-and-roles","Combining Scopes and Roles",[657,712],"handler := rocco.NewHandler[rocco.NoBody, AdminData](\n    \"admin-dashboard\",\n    \"GET\",\n    \"/admin/dashboard\",\n    getAdminDashboard,\n).\n    WithScopes(\"admin:read\").   // Must have admin:read scope\n    WithRoles(\"admin\", \"super\") // Must have admin OR super role",{"id":721,"title":722,"titles":723,"content":724,"level":20},"/v0.1.21/guides/authentication#usage-limits","Usage Limits",[657],"Rate limit based on identity statistics: handler := rocco.NewHandler[Input, Output](\n    \"create-resource\",\n    \"POST\",\n    \"/resources\",\n    createResource,\n).WithUsageLimit(\"api_calls_today\", func(id rocco.Identity) int {\n    // Return limit based on identity\n    if id.HasRole(\"premium\") {\n        return 10000\n    }\n    return 100\n}) The handler checks identity.Stats()[\"api_calls_today\"] against the limit. If exceeded, returns 429 Too Many Requests.",{"id":726,"title":727,"titles":728,"content":729,"level":31},"/v0.1.21/guides/authentication#multiple-limits","Multiple Limits",[657,722],"handler.\n    WithUsageLimit(\"api_calls_today\", dailyLimit).\n    WithUsageLimit(\"storage_mb\", storageLimit).\n    WithUsageLimit(\"concurrent_jobs\", concurrencyLimit)",{"id":731,"title":732,"titles":733,"content":734,"level":20},"/v0.1.21/guides/authentication#accessing-identity-in-handlers","Accessing Identity in Handlers",[657],"func(req *rocco.Request[Input]) (Output, error) {\n    // Get identity (nil if handler doesn't require auth)\n    identity := req.Identity\n\n    // Check auth status\n    if identity == nil {\n        // Handler allows anonymous access\n    }\n\n    // Access identity data\n    userID := identity.ID()\n    tenantID := identity.TenantID()\n\n    // Check permissions programmatically\n    if identity.HasScope(\"admin:write\") {\n        // Admin logic\n    }\n\n    // Multi-tenancy: filter by tenant\n    users, _ := db.FindUsers(tenantID)\n\n    return Output{...}, nil\n}",{"id":736,"title":737,"titles":738,"content":739,"level":20},"/v0.1.21/guides/authentication#noidentity-type","NoIdentity Type",[657],"For handlers that don't require authentication, req.Identity is a NoIdentity: type NoIdentity struct{}\n\nfunc (NoIdentity) ID() string            { return \"\" }\nfunc (NoIdentity) TenantID() string      { return \"\" }\nfunc (NoIdentity) Email() string         { return \"\" }\nfunc (NoIdentity) Scopes() []string      { return nil }\nfunc (NoIdentity) Roles() []string       { return nil }\nfunc (NoIdentity) HasScope(string) bool  { return false }\nfunc (NoIdentity) HasRole(string) bool   { return false }\nfunc (NoIdentity) Stats() map[string]int { return nil }",{"id":741,"title":205,"titles":742,"content":743,"level":20},"/v0.1.21/guides/authentication#error-responses",[657],"ScenarioStatusErrorNo/invalid identity401ErrUnauthorizedMissing required scope403ErrForbidden (message: \"insufficient scope\")Missing required role403ErrForbidden (message: \"insufficient role\")Usage limit exceeded429ErrTooManyRequests",{"id":745,"title":746,"titles":747,"content":748,"level":20},"/v0.1.21/guides/authentication#events","Events",[657],"Authentication events are emitted via capitan: EventDescriptionAuthenticationFailedIdentity extraction failedAuthenticationSucceededIdentity extracted successfullyAuthorizationScopeDeniedScope check failedAuthorizationRoleDeniedRole check failedAuthorizationSucceededAuthorization passedRateLimitExceededUsage limit exceeded",{"id":750,"title":619,"titles":751,"content":25,"level":20},"/v0.1.21/guides/authentication#best-practices",[657],{"id":753,"title":754,"titles":755,"content":756,"level":31},"/v0.1.21/guides/authentication#_1-use-https-in-production","1. Use HTTPS in Production",[657,619],"Rocco doesn't handle TLS. Use a reverse proxy (nginx, Caddy) or cloud load balancer.",{"id":758,"title":759,"titles":760,"content":761,"level":31},"/v0.1.21/guides/authentication#_2-validate-tokens-properly","2. Validate Tokens Properly",[657,619],"// Use constant-time comparison\n// Set reasonable expiration times\n// Validate issuer and audience",{"id":763,"title":764,"titles":765,"content":766,"level":31},"/v0.1.21/guides/authentication#_3-implement-token-refresh","3. Implement Token Refresh",[657,619],"Don't rely on long-lived tokens. Implement refresh token flows.",{"id":768,"title":769,"titles":770,"content":771,"level":31},"/v0.1.21/guides/authentication#_4-log-authentication-failures","4. Log Authentication Failures",[657,619],"capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {\n    path, _ := rocco.PathKey.From(e)\n    err, _ := rocco.ErrorKey.From(e)\n    securityLog.Warn(\"auth failed\", \"path\", path, \"error\", err)\n})",{"id":773,"title":774,"titles":775,"content":776,"level":31},"/v0.1.21/guides/authentication#_5-multi-tenancy","5. Multi-Tenancy",[657,619],"Always filter data by tenant: func(req *rocco.Request[Input]) (Output, error) {\n    tenantID := req.Identity.TenantID()\n\n    // ALWAYS filter by tenant\n    data, _ := db.Query(\"SELECT * FROM items WHERE tenant_id = ?\", tenantID)\n\n    return Output{...}, nil\n}",{"id":778,"title":98,"titles":779,"content":780,"level":20},"/v0.1.21/guides/authentication#see-also",[657],"Best Practices - Security recommendationsAuthentication Cookbook - Implementation patternsEvents Reference - Auth event details html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":782,"title":63,"titles":783,"content":784,"level":9},"/v0.1.21/guides/openapi",[],"Schema generation, tags, and validation mapping",{"id":786,"title":787,"titles":788,"content":789,"level":9},"/v0.1.21/guides/openapi#openapi-generation-guide","OpenAPI Generation Guide",[],"Rocco automatically generates OpenAPI 3.1.0 specifications from your handlers. This guide covers customization, schema tags, and advanced patterns.",{"id":791,"title":792,"titles":793,"content":794,"level":20},"/v0.1.21/guides/openapi#default-endpoints","Default Endpoints",[787],"When you register any handler, rocco automatically sets up: /openapi - OpenAPI JSON specification/docs - Interactive Scalar documentation engine := rocco.NewEngine()\nengine.WithHandlers(handler)\nengine.Start(rocco.HostAll, 8080)\n\n// Visit http://localhost:8080/docs for interactive docs",{"id":796,"title":797,"titles":798,"content":799,"level":20},"/v0.1.21/guides/openapi#customizing-api-info","Customizing API Info",[787],"engine.WithOpenAPIInfo(openapi.Info{\n    Title:          \"My API\",\n    Description:    \"API for managing resources\",\n    Version:        \"1.0.0\",\n    TermsOfService: \"https://example.com/terms\",\n    Contact: &openapi.Contact{\n        Name:  \"API Support\",\n        URL:   \"https://example.com/support\",\n        Email: \"support@example.com\",\n    },\n    License: &openapi.License{\n        Name: \"MIT\",\n        URL:  \"https://opensource.org/licenses/MIT\",\n    },\n})",{"id":801,"title":802,"titles":803,"content":804,"level":20},"/v0.1.21/guides/openapi#tags","Tags",[787],"Tags group operations in the documentation:",{"id":806,"title":807,"titles":808,"content":809,"level":31},"/v0.1.21/guides/openapi#handler-tags","Handler Tags",[787,802],"handler := rocco.NewHandler[Input, Output](\n    \"create-user\",\n    \"POST\",\n    \"/users\",\n    createUser,\n).WithTags(\"users\", \"admin\")",{"id":811,"title":812,"titles":813,"content":814,"level":31},"/v0.1.21/guides/openapi#engine-tags-with-descriptions","Engine Tags (with descriptions)",[787,802],"engine.WithTag(\"users\", \"User management operations\")\nengine.WithTag(\"orders\", \"Order processing and tracking\")\nengine.WithTag(\"admin\", \"Administrative operations\")",{"id":816,"title":817,"titles":818,"content":819,"level":31},"/v0.1.21/guides/openapi#tag-groups","Tag Groups",[787,802],"Tag groups organize tags into hierarchical categories via the x-tagGroups vendor extension. Documentation tools like Redoc use these to create sidebar sections: engine.\n    WithTagGroup(\"Account\", \"users\", \"auth\").\n    WithTagGroup(\"Commerce\", \"orders\", \"payments\").\n    WithTagGroup(\"Admin\", \"admin\", \"audit\")",{"id":821,"title":822,"titles":823,"content":824,"level":20},"/v0.1.21/guides/openapi#handler-documentation","Handler Documentation",[787],"handler := rocco.NewHandler[CreateUserInput, UserOutput](\n    \"create-user\",\n    \"POST\",\n    \"/users\",\n    createUser,\n).\n    WithSummary(\"Create a new user\").\n    WithDescription(`\nCreates a new user account with the provided information.\n\nThe email must be unique across all users. If a user with the same\nemail already exists, a 409 Conflict error is returned.\n\n**Required permissions:** users:write\n`).\n    WithTags(\"users\")",{"id":826,"title":827,"titles":828,"content":829,"level":20},"/v0.1.21/guides/openapi#schema-generation","Schema Generation",[787],"Rocco generates JSON schemas from your Go types using sentinel.",{"id":831,"title":832,"titles":833,"content":834,"level":31},"/v0.1.21/guides/openapi#basic-type-mapping","Basic Type Mapping",[787,827],"Go TypeJSON Schema Typestringstringint, int32, int64integerfloat32, float64numberboolboolean[]Tarraystructobject*Tnullable Tmap[string]Tobject with additionalProperties",{"id":836,"title":837,"titles":838,"content":25,"level":31},"/v0.1.21/guides/openapi#struct-tags","Struct Tags",[787,827],{"id":840,"title":841,"titles":842,"content":843,"level":844},"/v0.1.21/guides/openapi#json-tag","JSON Tag",[787,827,837],"type User struct {\n    ID        string `json:\"id\"`           // Field name in JSON\n    FirstName string `json:\"first_name\"`   // Snake case\n    Email     string `json:\"email\"`\n    Internal  string `json:\"-\"`            // Excluded from schema\n}",4,{"id":846,"title":847,"titles":848,"content":849,"level":844},"/v0.1.21/guides/openapi#description-tag","Description Tag",[787,827,837],"type CreateUserInput struct {\n    Name  string `json:\"name\" description:\"User's full name\"`\n    Email string `json:\"email\" description:\"Primary email address\"`\n    Age   int    `json:\"age\" description:\"Age in years\"`\n}",{"id":851,"title":852,"titles":853,"content":854,"level":844},"/v0.1.21/guides/openapi#example-tag","Example Tag",[787,827,837],"type CreateUserInput struct {\n    Name  string `json:\"name\" example:\"John Doe\"`\n    Email string `json:\"email\" example:\"john@example.com\"`\n    Age   int    `json:\"age\" example:\"25\"`\n} Examples are type-aware: String: example:\"hello\" → \"hello\"Integer: example:\"42\" → 42Float: example:\"3.14\" → 3.14Boolean: example:\"true\" → trueArray: example:\"a,b,c\" → [\"a\", \"b\", \"c\"]",{"id":856,"title":857,"titles":858,"content":859,"level":844},"/v0.1.21/guides/openapi#discriminator-tag","Discriminator Tag",[787,827,837],"type Notification struct {\n    Type  string `json:\"type\" discriminator:\"event\"` // \"I select for the event field\"\n    Event any    `json:\"event\" discriminate:\"TypeA,TypeB\"` // \"I am the union\"\n} See Discriminated Unions for full details.",{"id":861,"title":862,"titles":863,"content":864,"level":20},"/v0.1.21/guides/openapi#validation-to-openapi-mapping","Validation to OpenAPI Mapping",[787],"The validate tag drives both runtime validation and OpenAPI constraints: ValidatorApplies ToOpenAPI Mappingrequiredallrequired arraymin=Nnumbersminimummax=Nnumbersmaximummin=NstringsminLengthmax=NstringsmaxLengthgte=Nnumbersminimumlte=Nnumbersmaximumgt=Nnumbersminimum + exclusiveMinimumlt=Nnumbersmaximum + exclusiveMaximumlen=NarraysminItems + maxItemslen=NstringsminLength + maxLengthuniquearraysuniqueItemsemailstringsformat: \"email\"urlstringsformat: \"uri\"uuidstringsformat: \"uuid\"datetimestringsformat: \"date-time\"ipv4stringsformat: \"ipv4\"ipv6stringsformat: \"ipv6\"oneof=a b canyenum: [\"a\", \"b\", \"c\"]",{"id":866,"title":867,"titles":868,"content":869,"level":31},"/v0.1.21/guides/openapi#example","Example",[787,862],"type CreateUserInput struct {\n    Name     string   `json:\"name\" validate:\"required,min=2,max=100\" description:\"Full name\"`\n    Email    string   `json:\"email\" validate:\"required,email\" description:\"Email address\"`\n    Age      int      `json:\"age\" validate:\"gte=0,lte=150\" description:\"Age in years\"`\n    Role     string   `json:\"role\" validate:\"oneof=admin user guest\" description:\"User role\"`\n    Tags     []string `json:\"tags\" validate:\"max=10,unique\" description:\"User tags\"`\n} Generated schema: CreateUserInput:\n  type: object\n  required: [name, email]\n  properties:\n    name:\n      type: string\n      minLength: 2\n      maxLength: 100\n      description: Full name\n    email:\n      type: string\n      format: email\n      description: Email address\n    age:\n      type: integer\n      minimum: 0\n      maximum: 150\n      description: Age in years\n    role:\n      type: string\n      enum: [admin, user, guest]\n      description: User role\n    tags:\n      type: array\n      maxItems: 10\n      uniqueItems: true\n      description: User tags",{"id":871,"title":872,"titles":873,"content":874,"level":20},"/v0.1.21/guides/openapi#standalone-models","Standalone Models",[787],"Types that aren't directly used as handler input or output can be registered as standalone models. This is required for discriminated unions and useful for any type you want in your OpenAPI component schemas. engine.WithModels(\n    rocco.NewModel[IngestCompletedEvent](),\n    rocco.NewModel[IngestFailedEvent](),\n)",{"id":876,"title":877,"titles":878,"content":879,"level":20},"/v0.1.21/guides/openapi#discriminated-unions","Discriminated Unions",[787],"For polymorphic payloads where a type field determines the shape of a nested object, use the discriminator and discriminate struct tags together.",{"id":881,"title":802,"titles":882,"content":883,"level":31},"/v0.1.21/guides/openapi#tags-1",[787,877],"discriminator:\"fieldName\" on the selector field — declares \"I select for this union field\" and names the json property name of the target union field (not the Go field name)discriminate:\"TypeA,TypeB\" on the union field — declares \"I am the union\" and lists the possible schemas type Notification struct {\n    Type  string `json:\"type\" discriminator:\"event\"`\n    Event any    `json:\"event\" discriminate:\"IngestCompletedEvent,IngestFailedEvent\"`\n}\n\ntype IngestCompletedEvent struct {\n    DocumentID   string `json:\"document_id\"`\n    DocumentName string `json:\"document_name\"`\n}\n\ntype IngestFailedEvent struct {\n    DocumentID string `json:\"document_id\"`\n    Error      string `json:\"error\"`\n} Register the variant types as standalone models to ensure they appear in the spec (this is only strictly necessary when the types aren't already scanned through handler input/output types): engine := rocco.NewEngine().\n    WithModels(\n        rocco.NewModel[IngestCompletedEvent](),\n        rocco.NewModel[IngestFailedEvent](),\n    ).\n    WithHandlers(notificationHandler)",{"id":885,"title":886,"titles":887,"content":888,"level":31},"/v0.1.21/guides/openapi#generated-schema","Generated Schema",[787,877],"Notification:\n  type: object\n  properties:\n    type:\n      type: string\n    event:\n      oneOf:\n        - $ref: '#/components/schemas/IngestCompletedEvent'\n        - $ref: '#/components/schemas/IngestFailedEvent'\n      discriminator:\n        propertyName: type\n        mapping:\n          IngestCompletedEvent: '#/components/schemas/IngestCompletedEvent'\n          IngestFailedEvent: '#/components/schemas/IngestFailedEvent'",{"id":890,"title":891,"titles":892,"content":893,"level":20},"/v0.1.21/guides/openapi#error-schemas","Error Schemas",[787],"Declared errors generate response schemas: handler.WithErrors(rocco.ErrNotFound, rocco.ErrConflict) Generates: responses:\n  404:\n    description: Not Found\n    content:\n      application/json:\n        schema:\n          $ref: '#/components/schemas/ErrNotFound'\n  409:\n    description: Conflict\n    content:\n      application/json:\n        schema:\n          $ref: '#/components/schemas/ErrConflict'",{"id":895,"title":896,"titles":897,"content":898,"level":31},"/v0.1.21/guides/openapi#custom-error-schemas","Custom Error Schemas",[787,891],"type InsufficientFundsDetails struct {\n    Required  float64 `json:\"required\" description:\"Amount required\"`\n    Available float64 `json:\"available\" description:\"Amount available\"`\n}\n\nvar ErrInsufficientFunds = rocco.NewError[InsufficientFundsDetails](\n    \"INSUFFICIENT_FUNDS\", 402, \"insufficient funds\",\n)\n\nhandler.WithErrors(ErrInsufficientFunds) The details fields are inlined directly on the error schema as the details property.",{"id":900,"title":375,"titles":901,"content":25,"level":20},"/v0.1.21/guides/openapi#parameters",[787],{"id":903,"title":409,"titles":904,"content":905,"level":31},"/v0.1.21/guides/openapi#path-parameters",[787,375],"handler := rocco.NewHandler[rocco.NoBody, User](\n    \"get-user\",\n    \"GET\",\n    \"/users/{id}\",\n    getUser,\n).WithPathParams(\"id\") Generates: parameters:\n  - name: id\n    in: path\n    required: true\n    schema:\n      type: string",{"id":907,"title":414,"titles":908,"content":909,"level":31},"/v0.1.21/guides/openapi#query-parameters",[787,375],"handler := rocco.NewHandler[rocco.NoBody, UserList](\n    \"list-users\",\n    \"GET\",\n    \"/users\",\n    listUsers,\n).WithQueryParams(\"page\", \"limit\", \"sort\") Generates: parameters:\n  - name: page\n    in: query\n    schema:\n      type: string\n  - name: limit\n    in: query\n    schema:\n      type: string\n  - name: sort\n    in: query\n    schema:\n      type: string",{"id":911,"title":912,"titles":913,"content":914,"level":20},"/v0.1.21/guides/openapi#security-schemes","Security Schemes",[787],"For authenticated handlers: handler.WithAuthentication()\nhandler.WithScopes(\"users:read\") The OpenAPI spec includes security requirements.",{"id":916,"title":917,"titles":918,"content":919,"level":20},"/v0.1.21/guides/openapi#programmatic-access","Programmatic Access",[787],"Generate the spec programmatically: spec := engine.GenerateOpenAPI(nil)\n\n// Serialize to JSON\ndata, _ := json.MarshalIndent(spec, \"\", \"  \")\n\n// Write to file\nos.WriteFile(\"openapi.json\", data, 0644)",{"id":921,"title":619,"titles":922,"content":25,"level":20},"/v0.1.21/guides/openapi#best-practices",[787],{"id":924,"title":925,"titles":926,"content":927,"level":31},"/v0.1.21/guides/openapi#_1-use-descriptive-names","1. Use Descriptive Names",[787,619],"// GOOD - clear handler names\nNewHandler[Input, Output](\"create-user\", ...)\nNewHandler[Input, Output](\"list-user-orders\", ...)\n\n// AVOID - vague names\nNewHandler[Input, Output](\"handler1\", ...)",{"id":929,"title":930,"titles":931,"content":932,"level":31},"/v0.1.21/guides/openapi#_2-document-all-fields","2. Document All Fields",[787,619],"type CreateUserInput struct {\n    Name  string `json:\"name\" description:\"User's full legal name\" example:\"John Doe\"`\n    Email string `json:\"email\" description:\"Primary email for notifications\" example:\"john@example.com\"`\n}",{"id":934,"title":935,"titles":936,"content":937,"level":31},"/v0.1.21/guides/openapi#_3-use-consistent-tags","3. Use Consistent Tags",[787,619],"// Group by resource\nhandler.WithTags(\"users\")\nhandler.WithTags(\"orders\")\n\n// Add descriptions\nengine.WithTag(\"users\", \"User management operations\")",{"id":939,"title":940,"titles":941,"content":942,"level":31},"/v0.1.21/guides/openapi#_4-validate-everything","4. Validate Everything",[787,619],"type Input struct {\n    Email string `json:\"email\" validate:\"required,email\"`  // Both validated AND documented\n}",{"id":944,"title":945,"titles":946,"content":947,"level":31},"/v0.1.21/guides/openapi#_5-declare-all-errors","5. Declare All Errors",[787,619],"handler.WithErrors(\n    rocco.ErrNotFound,\n    rocco.ErrConflict,\n    rocco.ErrForbidden,\n) // All appear in OpenAPI spec",{"id":949,"title":98,"titles":950,"content":951,"level":20},"/v0.1.21/guides/openapi#see-also",[787],"Handler Guide - Handler configurationError Handling - Error patternsAPI Reference - Complete API documentation html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":953,"title":619,"titles":954,"content":955,"level":9},"/v0.1.21/guides/best-practices",[],"Security, performance, and production patterns",{"id":957,"title":958,"titles":959,"content":960,"level":9},"/v0.1.21/guides/best-practices#best-practices-guide","Best Practices Guide",[],"Production recommendations for security, performance, and maintainability.",{"id":962,"title":963,"titles":964,"content":25,"level":20},"/v0.1.21/guides/best-practices#security","Security",[958],{"id":966,"title":967,"titles":968,"content":969,"level":31},"/v0.1.21/guides/best-practices#https","HTTPS",[958,963],"Rocco doesn't handle TLS directly. Use one of these approaches: // Behind reverse proxy (recommended)\nengine := rocco.NewEngine().WithAuthenticator(extractIdentity)\nengine.Start(rocco.HostLoopback, 8080)\n// nginx/Caddy terminates TLS, forwards to localhost:8080",{"id":971,"title":972,"titles":973,"content":974,"level":31},"/v0.1.21/guides/best-practices#request-size-limits","Request Size Limits",[958,963],"Set appropriate body size limits: // Default is 10MB - adjust per handler\nhandler.WithMaxBodySize(1 * 1024 * 1024)  // 1MB for normal requests\nhandler.WithMaxBodySize(100 * 1024 * 1024) // 100MB for file uploads",{"id":976,"title":977,"titles":978,"content":979,"level":31},"/v0.1.21/guides/best-practices#input-validation","Input Validation",[958,963],"Validate all inputs with struct tags: type Input struct {\n    // Validate length\n    Name string `json:\"name\" validate:\"required,min=1,max=100\"`\n\n    // Validate format\n    Email string `json:\"email\" validate:\"required,email\"`\n\n    // Validate range\n    Age int `json:\"age\" validate:\"gte=0,lte=150\"`\n\n    // Validate enum\n    Status string `json:\"status\" validate:\"oneof=active inactive pending\"`\n\n    // Validate nested\n    Address Address `json:\"address\" validate:\"required\"`\n}",{"id":981,"title":982,"titles":983,"content":984,"level":31},"/v0.1.21/guides/best-practices#sql-injection-prevention","SQL Injection Prevention",[958,963],"Always use parameterized queries: // WRONG - vulnerable to SQL injection\ndb.Query(\"SELECT * FROM users WHERE id = \" + req.Params.Path[\"id\"])\n\n// CORRECT - parameterized query\ndb.Query(\"SELECT * FROM users WHERE id = $1\", req.Params.Path[\"id\"])",{"id":986,"title":987,"titles":988,"content":989,"level":31},"/v0.1.21/guides/best-practices#error-information-leakage","Error Information Leakage",[958,963],"Don't expose internal errors to clients: // WRONG - exposes internal details\nif err != nil {\n    return Output{}, err\n}\n\n// CORRECT - hide internal details\nif err != nil {\n    log.Error(\"database error\", \"error\", err)\n    return Output{}, rocco.ErrInternalServer\n}",{"id":991,"title":992,"titles":993,"content":994,"level":31},"/v0.1.21/guides/best-practices#authentication-security","Authentication Security",[958,963],"func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    token := r.Header.Get(\"Authorization\")\n\n    // 1. Validate token format\n    if !strings.HasPrefix(token, \"Bearer \") {\n        return nil, errors.New(\"invalid authorization format\")\n    }\n\n    // 2. Verify token signature\n    claims, err := verifyToken(strings.TrimPrefix(token, \"Bearer \"))\n    if err != nil {\n        // Log for security monitoring\n        log.Warn(\"token verification failed\", \"error\", err)\n        return nil, err\n    }\n\n    // 3. Check expiration\n    if claims.ExpiresAt.Before(time.Now()) {\n        return nil, errors.New(\"token expired\")\n    }\n\n    return &UserIdentity{...}, nil\n}",{"id":996,"title":997,"titles":998,"content":999,"level":31},"/v0.1.21/guides/best-practices#multi-tenancy","Multi-Tenancy",[958,963],"Always filter by tenant: func(req *rocco.Request[Input]) (Output, error) {\n    tenantID := req.Identity.TenantID()\n\n    // ALWAYS include tenant filter\n    items, err := db.Query(\n        \"SELECT * FROM items WHERE tenant_id = $1\",\n        tenantID,\n    )\n\n    return Output{Items: items}, nil\n}",{"id":1001,"title":1002,"titles":1003,"content":25,"level":20},"/v0.1.21/guides/best-practices#performance","Performance",[958],{"id":1005,"title":1006,"titles":1007,"content":1008,"level":31},"/v0.1.21/guides/best-practices#handler-registration","Handler Registration",[958,1002],"Register all handlers before calling Start(): // GOOD - register all handlers first\nengine.WithHandlers(handler1, handler2, handler3)\nengine.Start(rocco.HostAll, 8080)\n\n// AVOID - registering during runtime (not thread-safe)\ngo func() {\n    engine.WithHandlers(newHandler) // Race condition!\n}()",{"id":1010,"title":1011,"titles":1012,"content":1013,"level":31},"/v0.1.21/guides/best-practices#body-size-limits","Body Size Limits",[958,1002],"Set appropriate limits to prevent memory exhaustion: // Default 10MB is often too large\nhandler.WithMaxBodySize(1 * 1024 * 1024) // 1MB for typical JSON",{"id":1015,"title":230,"titles":1016,"content":1017,"level":31},"/v0.1.21/guides/best-practices#output-validation",[958,1002],"Output validation is disabled by default. Enable only in development: // Development - catch bugs early\nhandler.WithOutputValidation()\n\n// Production - skip for performance\n// (don't call WithOutputValidation)",{"id":1019,"title":1020,"titles":1021,"content":1022,"level":31},"/v0.1.21/guides/best-practices#middleware-efficiency","Middleware Efficiency",[958,1002],"Keep middleware lightweight: // GOOD - minimal work\nfunc loggingMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        start := time.Now()\n        next.ServeHTTP(w, r)\n        log.Info(\"request\", \"path\", r.URL.Path, \"duration\", time.Since(start))\n    })\n}\n\n// AVOID - heavy operations in middleware\nfunc heavyMiddleware(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        // Don't do database queries here!\n        // Don't do complex computations here!\n        next.ServeHTTP(w, r)\n    })\n}",{"id":1024,"title":1025,"titles":1026,"content":1027,"level":31},"/v0.1.21/guides/best-practices#connection-timeouts","Connection Timeouts",[958,1002],"Configure appropriate timeouts: engine := rocco.NewEngine()\n// Default timeouts are 120s - adjust for your use case\n// Timeouts are configured in EngineConfig",{"id":1029,"title":1030,"titles":1031,"content":25,"level":20},"/v0.1.21/guides/best-practices#handler-design","Handler Design",[958],{"id":1033,"title":1034,"titles":1035,"content":1036,"level":31},"/v0.1.21/guides/best-practices#single-responsibility","Single Responsibility",[958,1030],"Each handler should do one thing: // GOOD - single purpose\nNewHandler(\"create-user\", \"POST\", \"/users\", createUser)\nNewHandler(\"get-user\", \"GET\", \"/users/{id}\", getUser)\n\n// AVOID - multiple purposes\nNewHandler(\"user-actions\", \"POST\", \"/users/action\", func(req) {\n    switch req.Body.Action {\n    case \"create\": ...\n    case \"update\": ...\n    case \"delete\": ...\n    }\n})",{"id":1038,"title":1039,"titles":1040,"content":1041,"level":31},"/v0.1.21/guides/best-practices#consistent-error-handling","Consistent Error Handling",[958,1030],"Establish error patterns and follow them: func(req *rocco.Request[Input]) (Output, error) {\n    // 1. Validate authorization\n    if !canAccess(req.Identity, req.Params.Path[\"id\"]) {\n        return Output{}, rocco.ErrForbidden\n    }\n\n    // 2. Fetch resource\n    item, err := db.Get(req.Params.Path[\"id\"])\n    if errors.Is(err, sql.ErrNoRows) {\n        return Output{}, rocco.ErrNotFound\n    }\n    if err != nil {\n        return Output{}, rocco.ErrInternalServer.WithCause(err)\n    }\n\n    // 3. Business logic\n    result, err := process(item)\n    if err != nil {\n        return Output{}, rocco.ErrInternalServer.WithCause(err)\n    }\n\n    return Output{Result: result}, nil\n}",{"id":1043,"title":1044,"titles":1045,"content":1046,"level":31},"/v0.1.21/guides/best-practices#declare-all-errors","Declare All Errors",[958,1030],"handler.WithErrors(\n    rocco.ErrNotFound,\n    rocco.ErrForbidden,\n    rocco.ErrConflict,\n    // List every error the handler may return\n)",{"id":1048,"title":1049,"titles":1050,"content":25,"level":20},"/v0.1.21/guides/best-practices#api-design","API Design",[958],{"id":1052,"title":1053,"titles":1054,"content":1055,"level":31},"/v0.1.21/guides/best-practices#consistent-naming","Consistent Naming",[958,1049],"// Resource-based paths\n\"/users\"           // Collection\n\"/users/{id}\"      // Single resource\n\"/users/{id}/orders\" // Nested collection\n\n// Consistent verbs\n\"GET /users\"       // List\n\"POST /users\"      // Create\n\"GET /users/{id}\"  // Read\n\"PUT /users/{id}\"  // Update (full)\n\"PATCH /users/{id}\" // Update (partial)\n\"DELETE /users/{id}\" // Delete",{"id":1057,"title":1058,"titles":1059,"content":1060,"level":31},"/v0.1.21/guides/best-practices#versioning","Versioning",[958,1049],"// URL versioning\n\"/v1/users\"\n\"/v2/users\"\n\n// Tag for grouping\nhandler.WithTags(\"v1\")",{"id":1062,"title":1063,"titles":1064,"content":1065,"level":31},"/v0.1.21/guides/best-practices#pagination","Pagination",[958,1049],"type ListOutput struct {\n    Items      []Item `json:\"items\"`\n    Total      int    `json:\"total\"`\n    Page       int    `json:\"page\"`\n    PageSize   int    `json:\"page_size\"`\n    HasMore    bool   `json:\"has_more\"`\n}\n\nhandler := rocco.NewHandler[rocco.NoBody, ListOutput](\n    \"list-items\",\n    \"GET\",\n    \"/items\",\n    listItems,\n).WithQueryParams(\"page\", \"page_size\", \"sort\", \"order\")",{"id":1067,"title":78,"titles":1068,"content":25,"level":20},"/v0.1.21/guides/best-practices#observability",[958],{"id":1070,"title":1071,"titles":1072,"content":1073,"level":31},"/v0.1.21/guides/best-practices#event-hooks","Event Hooks",[958,78],"import \"github.com/zoobz-io/capitan\"\n\n// Log all requests\ncapitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n    method, _ := rocco.MethodKey.From(e)\n    path, _ := rocco.PathKey.From(e)\n    status, _ := rocco.StatusCodeKey.From(e)\n    duration, _ := rocco.DurationMsKey.From(e)\n\n    log.Info(\"request\",\n        \"method\", method,\n        \"path\", path,\n        \"status\", status,\n        \"duration_ms\", duration,\n    )\n})\n\n// Alert on errors\ncapitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {\n    err, _ := rocco.ErrorKey.From(e)\n    alerting.Send(\"API error\", err)\n})",{"id":1075,"title":1076,"titles":1077,"content":1078,"level":31},"/v0.1.21/guides/best-practices#metrics","Metrics",[958,78],"capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    switch e.Signal {\n    case rocco.RequestCompleted:\n        metrics.Inc(\"http_requests_total\", e.Labels())\n        duration, _ := rocco.DurationMsKey.From(e)\n        metrics.Observe(\"http_request_duration_ms\", float64(duration))\n    case rocco.RequestFailed:\n        metrics.Inc(\"http_errors_total\", e.Labels())\n    }\n})",{"id":1080,"title":1081,"titles":1082,"content":1083,"level":20},"/v0.1.21/guides/best-practices#graceful-shutdown","Graceful Shutdown",[958],"func main() {\n    engine := rocco.NewEngine()\n    engine.WithHandlers(handlers...)\n\n    // Start server in goroutine\n    go func() {\n        if err := engine.Start(rocco.HostAll, 8080); err != nil {\n            log.Fatal(err)\n        }\n    }()\n\n    // Wait for shutdown signal\n    sigChan := make(chan os.Signal, 1)\n    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n    \u003C-sigChan\n\n    // Graceful shutdown with timeout\n    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n    defer cancel()\n\n    if err := engine.Shutdown(ctx); err != nil {\n        log.Error(\"shutdown error\", \"error\", err)\n    }\n}",{"id":1085,"title":1086,"titles":1087,"content":25,"level":20},"/v0.1.21/guides/best-practices#testing","Testing",[958],{"id":1089,"title":1090,"titles":1091,"content":1092,"level":31},"/v0.1.21/guides/best-practices#use-test-helpers","Use Test Helpers",[958,1086],"import rtesting \"github.com/zoobz-io/rocco/testing\"\n\nfunc TestCreateUser(t *testing.T) {\n    engine := rtesting.TestEngine()\n    engine.WithHandlers(createUserHandler)\n\n    capture := rtesting.ServeRequest(engine, \"POST\", \"/users\", CreateUserInput{\n        Name:  \"John\",\n        Email: \"john@example.com\",\n    })\n\n    rtesting.AssertStatus(t, capture, http.StatusCreated)\n}",{"id":1094,"title":1095,"titles":1096,"content":1097,"level":31},"/v0.1.21/guides/best-practices#test-error-cases","Test Error Cases",[958,1086],"func TestCreateUser_ValidationError(t *testing.T) {\n    capture := rtesting.ServeRequest(engine, \"POST\", \"/users\", CreateUserInput{\n        Name:  \"\", // Invalid - required\n        Email: \"not-an-email\", // Invalid format\n    })\n\n    rtesting.AssertStatus(t, capture, http.StatusUnprocessableEntity)\n    rtesting.AssertErrorCode(t, capture, \"VALIDATION_FAILED\")\n}",{"id":1099,"title":1100,"titles":1101,"content":1102,"level":31},"/v0.1.21/guides/best-practices#test-authentication","Test Authentication",[958,1086],"func TestProtectedEndpoint(t *testing.T) {\n    // Without auth\n    capture := rtesting.ServeRequest(engine, \"GET\", \"/protected\", nil)\n    rtesting.AssertStatus(t, capture, http.StatusUnauthorized)\n\n    // With auth\n    capture = rtesting.ServeRequestWithHeaders(engine, \"GET\", \"/protected\", nil, map[string]string{\n        \"Authorization\": \"Bearer valid-token\",\n    })\n    rtesting.AssertStatus(t, capture, http.StatusOK)\n}",{"id":1104,"title":98,"titles":1105,"content":1106,"level":20},"/v0.1.21/guides/best-practices#see-also",[958],"Security Documentation - Security policyObservability Cookbook - Logging and metricsTesting Helpers - Test utilities html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":1108,"title":1109,"titles":1110,"content":1111,"level":9},"/v0.1.21/guides/streaming","Streaming (SSE)",[],"Server-Sent Events for real-time data streaming",{"id":1113,"title":1114,"titles":1115,"content":1116,"level":9},"/v0.1.21/guides/streaming#streaming-guide","Streaming Guide",[],"Rocco provides built-in support for Server-Sent Events (SSE), enabling real-time data streaming from server to client over HTTP.",{"id":1118,"title":1119,"titles":1120,"content":1121,"level":20},"/v0.1.21/guides/streaming#when-to-use-sse","When to Use SSE",[1114],"Use CaseSSEWebSocketPollingServer → Client updates✓ Best✓✓Bidirectional communication✗✓ Best✗Browser reconnection✓ Built-in✗ ManualN/AProxy/firewall compatibility✓ Excellent✗ Can be blocked✓Binary data✗✓✓ SSE is ideal for: notifications, live feeds, progress updates, dashboards, and any scenario where the server pushes updates to clients.",{"id":1123,"title":1124,"titles":1125,"content":25,"level":20},"/v0.1.21/guides/streaming#basic-usage","Basic Usage",[1114],{"id":1127,"title":1128,"titles":1129,"content":1130,"level":31},"/v0.1.21/guides/streaming#creating-a-stream-handler","Creating a Stream Handler",[1114,1124],"type PriceUpdate struct {\n    Symbol string  `json:\"symbol\"`\n    Price  float64 `json:\"price\"`\n}\n\nhandler := rocco.NewStreamHandler[rocco.NoBody, PriceUpdate](\n    \"price-stream\",\n    http.MethodGet,\n    \"/prices/stream\",\n    func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[PriceUpdate]) error {\n        ticker := time.NewTicker(time.Second)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-stream.Done():\n                // Client disconnected\n                return nil\n            case \u003C-ticker.C:\n                if err := stream.Send(PriceUpdate{\n                    Symbol: \"BTC\",\n                    Price:  getCurrentPrice(),\n                }); err != nil {\n                    return err\n                }\n            }\n        }\n    },\n).WithSummary(\"Stream price updates\")",{"id":1132,"title":1133,"titles":1134,"content":1135,"level":31},"/v0.1.21/guides/streaming#the-stream-interface","The Stream Interface",[1114,1124],"type Stream[T any] interface {\n    // Send sends a data-only event\n    Send(data T) error\n\n    // SendEvent sends a named event with data\n    SendEvent(event string, data T) error\n\n    // SendComment sends a comment (useful for keep-alive)\n    SendComment(comment string) error\n\n    // Done returns a channel closed when client disconnects\n    Done() \u003C-chan struct{}\n}",{"id":1137,"title":1138,"titles":1139,"content":25,"level":20},"/v0.1.21/guides/streaming#event-types","Event Types",[1114],{"id":1141,"title":1142,"titles":1143,"content":1144,"level":31},"/v0.1.21/guides/streaming#data-events","Data Events",[1114,1138],"The simplest form - sends JSON data: stream.Send(PriceUpdate{Symbol: \"ETH\", Price: 2500.00}) Output: data: {\"symbol\":\"ETH\",\"price\":2500}",{"id":1146,"title":1147,"titles":1148,"content":1149,"level":31},"/v0.1.21/guides/streaming#named-events","Named Events",[1114,1138],"Named events allow clients to filter by event type: stream.SendEvent(\"price\", PriceUpdate{Symbol: \"ETH\", Price: 2500.00})\nstream.SendEvent(\"volume\", VolumeUpdate{Symbol: \"ETH\", Volume: 1000000}) Output: event: price\ndata: {\"symbol\":\"ETH\",\"price\":2500}\n\nevent: volume\ndata: {\"symbol\":\"ETH\",\"volume\":1000000} Client-side handling: const source = new EventSource('/prices/stream');\n\nsource.addEventListener('price', (e) => {\n    const price = JSON.parse(e.data);\n    console.log('Price update:', price);\n});\n\nsource.addEventListener('volume', (e) => {\n    const volume = JSON.parse(e.data);\n    console.log('Volume update:', volume);\n});",{"id":1151,"title":1152,"titles":1153,"content":1154,"level":31},"/v0.1.21/guides/streaming#comments-keep-alive","Comments (Keep-Alive)",[1114,1138],"Comments are ignored by clients but keep the connection alive: // Send keep-alive every 30 seconds\nticker := time.NewTicker(30 * time.Second)\nfor {\n    select {\n    case \u003C-ticker.C:\n        stream.SendComment(\"keep-alive\")\n    case data := \u003C-updates:\n        stream.Send(data)\n    case \u003C-stream.Done():\n        return nil\n    }\n} Output: : keep-alive",{"id":1156,"title":1157,"titles":1158,"content":1159,"level":20},"/v0.1.21/guides/streaming#client-disconnection","Client Disconnection",[1114],"Always check stream.Done() to detect client disconnection: func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Event]) error {\n    for {\n        select {\n        case \u003C-stream.Done():\n            // Clean up resources\n            log.Println(\"Client disconnected\")\n            return nil\n        case event := \u003C-eventChannel:\n            if err := stream.Send(event); err != nil {\n                return err\n            }\n        }\n    }\n}",{"id":1161,"title":1162,"titles":1163,"content":1164,"level":20},"/v0.1.21/guides/streaming#authentication","Authentication",[1114],"Stream handlers support the same authentication as regular handlers: handler := rocco.NewStreamHandler[rocco.NoBody, Notification](\n    \"notifications\",\n    http.MethodGet,\n    \"/notifications/stream\",\n    func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Notification]) error {\n        userID := req.Identity.ID()\n        // Stream notifications for this user\n        for notification := range getUserNotifications(userID) {\n            stream.Send(notification)\n        }\n        return nil\n    },\n).\n    WithAuthentication().\n    WithScopes(\"notifications:read\")",{"id":1166,"title":1167,"titles":1168,"content":1169,"level":20},"/v0.1.21/guides/streaming#request-input","Request Input",[1114],"Stream handlers can accept request bodies (useful for POST streams with initial configuration): type StreamConfig struct {\n    Symbols  []string `json:\"symbols\" validate:\"required,min=1\"`\n    Interval int      `json:\"interval\" validate:\"min=100,max=60000\"`\n}\n\nhandler := rocco.NewStreamHandler[StreamConfig, PriceUpdate](\n    \"configured-stream\",\n    http.MethodPost,\n    \"/prices/stream\",\n    func(req *rocco.Request[StreamConfig], stream rocco.Stream[PriceUpdate]) error {\n        ticker := time.NewTicker(time.Duration(req.Body.Interval) * time.Millisecond)\n        defer ticker.Stop()\n\n        for {\n            select {\n            case \u003C-stream.Done():\n                return nil\n            case \u003C-ticker.C:\n                for _, symbol := range req.Body.Symbols {\n                    stream.Send(PriceUpdate{\n                        Symbol: symbol,\n                        Price:  getPrice(symbol),\n                    })\n                }\n            }\n        }\n    },\n)",{"id":1171,"title":1172,"titles":1173,"content":1174,"level":20},"/v0.1.21/guides/streaming#path-and-query-parameters","Path and Query Parameters",[1114],"handler := rocco.NewStreamHandler[rocco.NoBody, Event](\n    \"channel-stream\",\n    http.MethodGet,\n    \"/channels/{channel}/events\",\n    func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Event]) error {\n        channel := req.Params.Path[\"channel\"]\n        filter := req.Params.Query[\"filter\"]\n\n        events := subscribeToChannel(channel, filter)\n        for event := range events {\n            stream.Send(event)\n        }\n        return nil\n    },\n).\n    WithPathParams(\"channel\").\n    WithQueryParams(\"filter\")",{"id":1176,"title":1177,"titles":1178,"content":1179,"level":20},"/v0.1.21/guides/streaming#openapi-documentation","OpenAPI Documentation",[1114],"Stream handlers are documented in OpenAPI with text/event-stream content type: /prices/stream:\n  get:\n    summary: Stream price updates\n    responses:\n      '200':\n        description: Server-Sent Events stream\n        content:\n          text/event-stream:\n            schema:\n              type: string\n              description: SSE stream emitting PriceUpdate events as JSON",{"id":1181,"title":619,"titles":1182,"content":25,"level":20},"/v0.1.21/guides/streaming#best-practices",[1114],{"id":1184,"title":1185,"titles":1186,"content":1187,"level":31},"/v0.1.21/guides/streaming#_1-always-handle-disconnection","1. Always Handle Disconnection",[1114,619],"select {\ncase \u003C-stream.Done():\n    return nil  // Clean exit\ncase data := \u003C-source:\n    stream.Send(data)\n}",{"id":1189,"title":1190,"titles":1191,"content":1192,"level":31},"/v0.1.21/guides/streaming#_2-use-keep-alives-for-long-lived-streams","2. Use Keep-Alives for Long-Lived Streams",[1114,619],"keepAlive := time.NewTicker(30 * time.Second)\ndefer keepAlive.Stop()\n\nfor {\n    select {\n    case \u003C-keepAlive.C:\n        stream.SendComment(\"ping\")\n    // ... other cases\n    }\n}",{"id":1194,"title":1195,"titles":1196,"content":1197,"level":31},"/v0.1.21/guides/streaming#_3-clean-up-resources","3. Clean Up Resources",[1114,619],"func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Event]) error {\n    subscription := subscribe()\n    defer subscription.Close()  // Always clean up\n\n    for {\n        select {\n        case \u003C-stream.Done():\n            return nil\n        case event := \u003C-subscription.Events():\n            stream.Send(event)\n        }\n    }\n}",{"id":1199,"title":1200,"titles":1201,"content":1202,"level":31},"/v0.1.21/guides/streaming#_4-consider-backpressure","4. Consider Backpressure",[1114,619],"If events arrive faster than they can be sent: for {\n    select {\n    case \u003C-stream.Done():\n        return nil\n    case event := \u003C-fastSource:\n        // Non-blocking send with timeout\n        ctx, cancel := context.WithTimeout(req.Context, time.Second)\n        err := stream.Send(event)\n        cancel()\n        if err != nil {\n            return err  // Client too slow, disconnect\n        }\n    }\n}",{"id":1204,"title":78,"titles":1205,"content":1206,"level":20},"/v0.1.21/guides/streaming#observability",[1114],"Stream handlers emit lifecycle signals: SignalWhenhttp.stream.executingHandler startshttp.stream.startedHeaders sent, stream establishedhttp.stream.endedHandler completed normallyhttp.stream.client.disconnectedClient disconnectedhttp.stream.errorError during streaming Hook into these for metrics and logging: capitan.Subscribe(rocco.StreamStarted, func(fields map[string]any) {\n    metrics.StreamsActive.Inc()\n})\n\ncapitan.Subscribe(rocco.StreamEnded, func(fields map[string]any) {\n    metrics.StreamsActive.Dec()\n}) html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":1208,"title":1209,"titles":1210,"content":1211,"level":9},"/v0.1.21/cookbook/crud-api","CRUD API",[],"Complete CRUD API example with all patterns",{"id":1213,"title":1214,"titles":1215,"content":1216,"level":9},"/v0.1.21/cookbook/crud-api#crud-api-cookbook","CRUD API Cookbook",[],"A complete example of building a RESTful CRUD API with rocco.",{"id":1218,"title":1219,"titles":1220,"content":1221,"level":20},"/v0.1.21/cookbook/crud-api#domain-model","Domain Model",[1214],"We'll build a product management API: // Domain types\ntype Product struct {\n    ID          string    `json:\"id\"`\n    Name        string    `json:\"name\"`\n    Description string    `json:\"description\"`\n    Price       float64   `json:\"price\"`\n    Stock       int       `json:\"stock\"`\n    CreatedAt   time.Time `json:\"created_at\"`\n    UpdatedAt   time.Time `json:\"updated_at\"`\n}\n\n// Input types\ntype CreateProductInput struct {\n    Name        string  `json:\"name\" validate:\"required,min=1,max=200\" description:\"Product name\"`\n    Description string  `json:\"description\" validate:\"max=2000\" description:\"Product description\"`\n    Price       float64 `json:\"price\" validate:\"required,gt=0\" description:\"Price in USD\"`\n    Stock       int     `json:\"stock\" validate:\"gte=0\" description:\"Available stock\"`\n}\n\ntype UpdateProductInput struct {\n    Name        *string  `json:\"name\" validate:\"omitempty,min=1,max=200\"`\n    Description *string  `json:\"description\" validate:\"omitempty,max=2000\"`\n    Price       *float64 `json:\"price\" validate:\"omitempty,gt=0\"`\n    Stock       *int     `json:\"stock\" validate:\"omitempty,gte=0\"`\n}\n\n// Output types\ntype ProductListOutput struct {\n    Products []Product `json:\"products\"`\n    Total    int       `json:\"total\"`\n    Page     int       `json:\"page\"`\n    PageSize int       `json:\"page_size\"`\n}\n\ntype DeleteOutput struct {\n    Deleted bool `json:\"deleted\"`\n}",{"id":1223,"title":346,"titles":1224,"content":25,"level":20},"/v0.1.21/cookbook/crud-api#handlers",[1214],{"id":1226,"title":1227,"titles":1228,"content":1229,"level":31},"/v0.1.21/cookbook/crud-api#create-product","Create Product",[1214,346],"var createProduct = rocco.NewHandler[CreateProductInput, Product](\n    \"create-product\",\n    \"POST\",\n    \"/products\",\n    func(req *rocco.Request[CreateProductInput]) (Product, error) {\n        product := Product{\n            ID:          generateID(),\n            Name:        req.Body.Name,\n            Description: req.Body.Description,\n            Price:       req.Body.Price,\n            Stock:       req.Body.Stock,\n            CreatedAt:   time.Now(),\n            UpdatedAt:   time.Now(),\n        }\n\n        if err := db.CreateProduct(&product); err != nil {\n            if isUniqueViolation(err) {\n                return Product{}, rocco.ErrConflict.\n                    WithMessage(\"product name already exists\").\n                    WithDetails(rocco.ConflictDetails{\n                        Reason: \"a product with this name already exists\",\n                    })\n            }\n            return Product{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return product, nil\n    },\n).\n    WithSummary(\"Create a new product\").\n    WithDescription(\"Creates a new product in the catalog\").\n    WithTags(\"products\").\n    WithSuccessStatus(201).\n    WithErrors(rocco.ErrConflict)",{"id":1231,"title":1232,"titles":1233,"content":1234,"level":31},"/v0.1.21/cookbook/crud-api#list-products","List Products",[1214,346],"var listProducts = rocco.NewHandler[rocco.NoBody, ProductListOutput](\n    \"list-products\",\n    \"GET\",\n    \"/products\",\n    func(req *rocco.Request[rocco.NoBody]) (ProductListOutput, error) {\n        // Parse pagination params\n        page := parseIntOrDefault(req.Params.Query[\"page\"], 1)\n        pageSize := parseIntOrDefault(req.Params.Query[\"page_size\"], 20)\n        if pageSize > 100 {\n            pageSize = 100 // Cap at 100\n        }\n\n        // Parse filters\n        search := req.Params.Query[\"search\"]\n        minPrice := parseFloatOrDefault(req.Params.Query[\"min_price\"], 0)\n        maxPrice := parseFloatOrDefault(req.Params.Query[\"max_price\"], 0)\n\n        // Query products\n        products, total, err := db.ListProducts(ListOptions{\n            Page:     page,\n            PageSize: pageSize,\n            Search:   search,\n            MinPrice: minPrice,\n            MaxPrice: maxPrice,\n        })\n        if err != nil {\n            return ProductListOutput{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return ProductListOutput{\n            Products: products,\n            Total:    total,\n            Page:     page,\n            PageSize: pageSize,\n        }, nil\n    },\n).\n    WithSummary(\"List products\").\n    WithDescription(\"Returns a paginated list of products with optional filters\").\n    WithTags(\"products\").\n    WithQueryParams(\"page\", \"page_size\", \"search\", \"min_price\", \"max_price\")",{"id":1236,"title":1237,"titles":1238,"content":1239,"level":31},"/v0.1.21/cookbook/crud-api#get-product","Get Product",[1214,346],"var getProduct = rocco.NewHandler[rocco.NoBody, Product](\n    \"get-product\",\n    \"GET\",\n    \"/products/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (Product, error) {\n        product, err := db.GetProduct(req.Params.Path[\"id\"])\n        if err != nil {\n            if errors.Is(err, sql.ErrNoRows) {\n                return Product{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n                    Resource: \"product\",\n                })\n            }\n            return Product{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return *product, nil\n    },\n).\n    WithSummary(\"Get a product\").\n    WithDescription(\"Returns a single product by ID\").\n    WithTags(\"products\").\n    WithPathParams(\"id\").\n    WithErrors(rocco.ErrNotFound)",{"id":1241,"title":1242,"titles":1243,"content":1244,"level":31},"/v0.1.21/cookbook/crud-api#update-product","Update Product",[1214,346],"var updateProduct = rocco.NewHandler[UpdateProductInput, Product](\n    \"update-product\",\n    \"PUT\",\n    \"/products/{id}\",\n    func(req *rocco.Request[UpdateProductInput]) (Product, error) {\n        // Get existing product\n        product, err := db.GetProduct(req.Params.Path[\"id\"])\n        if err != nil {\n            if errors.Is(err, sql.ErrNoRows) {\n                return Product{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n                    Resource: \"product\",\n                })\n            }\n            return Product{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        // Apply updates (only non-nil fields)\n        if req.Body.Name != nil {\n            product.Name = *req.Body.Name\n        }\n        if req.Body.Description != nil {\n            product.Description = *req.Body.Description\n        }\n        if req.Body.Price != nil {\n            product.Price = *req.Body.Price\n        }\n        if req.Body.Stock != nil {\n            product.Stock = *req.Body.Stock\n        }\n        product.UpdatedAt = time.Now()\n\n        // Save\n        if err := db.UpdateProduct(product); err != nil {\n            if isUniqueViolation(err) {\n                return Product{}, rocco.ErrConflict.\n                    WithMessage(\"product name already exists\").\n                    WithDetails(rocco.ConflictDetails{\n                        Reason: \"a product with this name already exists\",\n                    })\n            }\n            return Product{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return *product, nil\n    },\n).\n    WithSummary(\"Update a product\").\n    WithDescription(\"Updates an existing product. Only provided fields are updated.\").\n    WithTags(\"products\").\n    WithPathParams(\"id\").\n    WithErrors(rocco.ErrNotFound, rocco.ErrConflict)",{"id":1246,"title":1247,"titles":1248,"content":1249,"level":31},"/v0.1.21/cookbook/crud-api#delete-product","Delete Product",[1214,346],"var deleteProduct = rocco.NewHandler[rocco.NoBody, DeleteOutput](\n    \"delete-product\",\n    \"DELETE\",\n    \"/products/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (DeleteOutput, error) {\n        err := db.DeleteProduct(req.Params.Path[\"id\"])\n        if err != nil {\n            if errors.Is(err, sql.ErrNoRows) {\n                return DeleteOutput{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n                    Resource: \"product\",\n                })\n            }\n            return DeleteOutput{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return DeleteOutput{Deleted: true}, nil\n    },\n).\n    WithSummary(\"Delete a product\").\n    WithDescription(\"Permanently deletes a product\").\n    WithTags(\"products\").\n    WithPathParams(\"id\").\n    WithErrors(rocco.ErrNotFound)",{"id":1251,"title":1252,"titles":1253,"content":1254,"level":20},"/v0.1.21/cookbook/crud-api#complete-application","Complete Application",[1214],"package main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"os\"\n    \"os/signal\"\n    \"syscall\"\n    \"time\"\n\n    \"github.com/zoobz-io/openapi\"\n    \"github.com/zoobz-io/rocco\"\n)\n\nfunc main() {\n    // Create engine\n    engine := rocco.NewEngine()\n\n    // Configure OpenAPI\n    engine.WithOpenAPIInfo(openapi.Info{\n        Title:       \"Product API\",\n        Description: \"API for managing product catalog\",\n        Version:     \"1.0.0\",\n    })\n    engine.WithTag(\"products\", \"Product management operations\")\n\n    // Register handlers\n    engine.WithHandlers(\n        createProduct,\n        listProducts,\n        getProduct,\n        updateProduct,\n        deleteProduct,\n    )\n\n    // Start server\n    go func() {\n        fmt.Println(\"Server running at http://localhost:8080\")\n        fmt.Println(\"API docs at http://localhost:8080/docs\")\n        if err := engine.Start(rocco.HostAll, 8080); err != nil {\n            fmt.Printf(\"Server error: %v\\n\", err)\n        }\n    }()\n\n    // Graceful shutdown\n    sigChan := make(chan os.Signal, 1)\n    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n    \u003C-sigChan\n\n    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\n    defer cancel()\n    engine.Shutdown(ctx)\n}",{"id":1256,"title":1086,"titles":1257,"content":1258,"level":20},"/v0.1.21/cookbook/crud-api#testing",[1214],"package main\n\nimport (\n    \"testing\"\n    \"net/http\"\n\n    rtesting \"github.com/zoobz-io/rocco/testing\"\n)\n\nfunc TestProductCRUD(t *testing.T) {\n    engine := rtesting.TestEngine()\n    engine.WithHandlers(createProduct, listProducts, getProduct, updateProduct, deleteProduct)\n\n    // Create\n    t.Run(\"Create\", func(t *testing.T) {\n        capture := rtesting.ServeRequest(engine, \"POST\", \"/products\", CreateProductInput{\n            Name:  \"Widget\",\n            Price: 9.99,\n            Stock: 100,\n        })\n        rtesting.AssertStatus(t, capture, http.StatusCreated)\n\n        var product Product\n        capture.DecodeJSON(&product)\n        if product.Name != \"Widget\" {\n            t.Errorf(\"expected name Widget, got %s\", product.Name)\n        }\n    })\n\n    // List\n    t.Run(\"List\", func(t *testing.T) {\n        capture := rtesting.ServeRequest(engine, \"GET\", \"/products?page=1&page_size=10\", nil)\n        rtesting.AssertStatus(t, capture, http.StatusOK)\n\n        var list ProductListOutput\n        capture.DecodeJSON(&list)\n        if list.Total \u003C 1 {\n            t.Error(\"expected at least 1 product\")\n        }\n    })\n\n    // Get\n    t.Run(\"Get\", func(t *testing.T) {\n        capture := rtesting.ServeRequest(engine, \"GET\", \"/products/prod_123\", nil)\n        rtesting.AssertStatus(t, capture, http.StatusOK)\n    })\n\n    // Get Not Found\n    t.Run(\"GetNotFound\", func(t *testing.T) {\n        capture := rtesting.ServeRequest(engine, \"GET\", \"/products/nonexistent\", nil)\n        rtesting.AssertStatus(t, capture, http.StatusNotFound)\n        rtesting.AssertErrorCode(t, capture, \"NOT_FOUND\")\n    })\n\n    // Update\n    t.Run(\"Update\", func(t *testing.T) {\n        newPrice := 19.99\n        capture := rtesting.ServeRequest(engine, \"PUT\", \"/products/prod_123\", UpdateProductInput{\n            Price: &newPrice,\n        })\n        rtesting.AssertStatus(t, capture, http.StatusOK)\n    })\n\n    // Delete\n    t.Run(\"Delete\", func(t *testing.T) {\n        capture := rtesting.ServeRequest(engine, \"DELETE\", \"/products/prod_123\", nil)\n        rtesting.AssertStatus(t, capture, http.StatusOK)\n    })\n}",{"id":1260,"title":1261,"titles":1262,"content":25,"level":20},"/v0.1.21/cookbook/crud-api#variations","Variations",[1214],{"id":1264,"title":1265,"titles":1266,"content":1267,"level":31},"/v0.1.21/cookbook/crud-api#with-authentication","With Authentication",[1214,1261],"var createProduct = rocco.NewHandler[CreateProductInput, Product](\n    \"create-product\",\n    \"POST\",\n    \"/products\",\n    createProductHandler,\n).\n    WithAuthentication().\n    WithScopes(\"products:write\")",{"id":1269,"title":1270,"titles":1271,"content":1272,"level":31},"/v0.1.21/cookbook/crud-api#with-tenant-isolation","With Tenant Isolation",[1214,1261],"func(req *rocco.Request[rocco.NoBody]) (ProductListOutput, error) {\n    tenantID := req.Identity.TenantID()\n\n    products, total, err := db.ListProducts(tenantID, options)\n    // ...\n}",{"id":1274,"title":1275,"titles":1276,"content":1277,"level":31},"/v0.1.21/cookbook/crud-api#soft-delete","Soft Delete",[1214,1261],"var deleteProduct = rocco.NewHandler[rocco.NoBody, DeleteOutput](\n    \"delete-product\",\n    \"DELETE\",\n    \"/products/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (DeleteOutput, error) {\n        err := db.SoftDeleteProduct(req.Params.Path[\"id\"])\n        // Sets deleted_at instead of removing row\n        return DeleteOutput{Deleted: true}, nil\n    },\n)",{"id":1279,"title":98,"titles":1280,"content":1281,"level":20},"/v0.1.21/cookbook/crud-api#see-also",[1214],"Handler Guide - Handler configurationError Handling - Error patternsAuthentication Cookbook - Adding auth html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .suWN2, html code.shiki .suWN2{--shiki-default:var(--shiki-tag)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":1283,"title":1284,"titles":1285,"content":1286,"level":9},"/v0.1.21/cookbook/authentication","Authentication Patterns",[],"Auth0, session-based OAuth, JWT, API keys, and multi-auth implementations",{"id":1288,"title":1289,"titles":1290,"content":1291,"level":9},"/v0.1.21/cookbook/authentication#authentication-cookbook","Authentication Cookbook",[],"Implementation patterns for common authentication scenarios.",{"id":1293,"title":1294,"titles":1295,"content":1296,"level":20},"/v0.1.21/cookbook/authentication#auth0-authentication","Auth0 Authentication",[1289],"The rocco/auth0 package provides drop-in Auth0 JWT authentication.",{"id":1298,"title":1299,"titles":1300,"content":1301,"level":31},"/v0.1.21/cookbook/authentication#basic-setup","Basic Setup",[1289,1294],"import (\n    \"github.com/zoobz-io/rocco\"\n    \"github.com/zoobz-io/rocco/auth0\"\n)\n\nfunc main() {\n    extractor, err := auth0.NewExtractor(auth0.Config{\n        Domain:   \"your-tenant.auth0.com\",\n        Audience: \"https://your-api.example.com\",\n    })\n    if err != nil {\n        log.Fatal(err)\n    }\n\n    engine := rocco.NewEngine().WithAuthenticator(extractor)\n\n    engine.WithHandlers(\n        rocco.NewHandler[rocco.NoBody, UserResponse](\n            \"get-profile\", \"GET\", \"/profile\",\n            func(req *rocco.Request[rocco.NoBody]) (UserResponse, error) {\n                return UserResponse{\n                    ID:    req.Identity.ID(),\n                    Email: req.Identity.Email(),\n                    Roles: req.Identity.Roles(),\n                }, nil\n            },\n        ).WithAuthentication(),\n    )\n\n    engine.Start(rocco.HostAll, 8080)\n}",{"id":1303,"title":1304,"titles":1305,"content":1306,"level":31},"/v0.1.21/cookbook/authentication#custom-claims-namespaced","Custom Claims (Namespaced)",[1289,1294],"Auth0 requires namespaced custom claims. Configure the claim paths: auth0.Config{\n    Domain:      \"your-tenant.auth0.com\",\n    Audience:    \"https://your-api.example.com\",\n    RolesClaim:  \"https://myapp.com/roles\",\n    TenantClaim: \"https://myapp.com/org_id\",\n}",{"id":1308,"title":1309,"titles":1310,"content":1311,"level":31},"/v0.1.21/cookbook/authentication#user-enrichment","User Enrichment",[1289,1294],"Enrich JWT identity with database user data: auth0Validator, _ := auth0.NewValidator(cfg)\n\nextractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // Validate JWT\n    identity, err := auth0Validator.Extractor()(ctx, r)\n    if err != nil {\n        return nil, err\n    }\n\n    // Look up or create user in database\n    user, err := userRepo.FindOrCreateBySubject(ctx, identity.ID(), identity.Email())\n    if err != nil {\n        return nil, err\n    }\n\n    return user, nil  // User implements rocco.Identity\n}\n\nengine := rocco.NewEngine().WithAuthenticator(extractor) Your User type implements rocco.Identity: type User struct {\n    id            string\n    email         string\n    tenantID      string\n    roles         []string\n    subscription  string  // app-specific data\n    requestsToday int     // for rate limiting\n}\n\nfunc (u *User) ID() string            { return u.id }\nfunc (u *User) TenantID() string      { return u.tenantID }\nfunc (u *User) Email() string         { return u.email }\nfunc (u *User) Scopes() []string      { return nil }\nfunc (u *User) Roles() []string       { return u.roles }\nfunc (u *User) HasScope(s string) bool { return false }\nfunc (u *User) HasRole(r string) bool {\n    for _, role := range u.roles {\n        if role == r {\n            return true\n        }\n    }\n    return false\n}\nfunc (u *User) Stats() map[string]int {\n    return map[string]int{\"requests_today\": u.requestsToday}\n}\nfunc (u *User) Subscription() string { return u.subscription }\n\n// Helper for type-safe access in handlers\nfunc UserFrom(id rocco.Identity) *User {\n    u, _ := id.(*User)\n    return u\n} Usage in handlers: func(req *rocco.Request[Input]) (Output, error) {\n    user := UserFrom(req.Identity)\n    if user.Subscription() == \"free\" {\n        // apply free tier limits\n    }\n    // ...\n}",{"id":1313,"title":1314,"titles":1315,"content":1316,"level":31},"/v0.1.21/cookbook/authentication#testing-with-auth0","Testing with Auth0",[1289,1294],"For testing, use rocco/testing.MockIdentity instead of real JWTs: import (\n    \"net/http/httptest\"\n    \"testing\"\n\n    \"github.com/zoobz-io/rocco\"\n    roccotest \"github.com/zoobz-io/rocco/testing\"\n)\n\nfunc TestProtectedEndpoint(t *testing.T) {\n    // Create mock identity extractor\n    mockIdentity := roccotest.NewMockIdentity(\"user-123\").\n        WithEmail(\"test@example.com\").\n        WithRoles(\"admin\")\n\n    extractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n        return mockIdentity, nil\n    }\n\n    engine := rocco.NewEngine().WithAuthenticator(extractor)\n    engine.WithHandlers(myProtectedHandler)\n\n    req := httptest.NewRequest(\"GET\", \"/protected\", nil)\n    w := httptest.NewRecorder()\n    engine.Router().ServeHTTP(w, req)\n\n    if w.Code != http.StatusOK {\n        t.Errorf(\"expected 200, got %d\", w.Code)\n    }\n} For integration tests requiring actual JWT validation, create a mock JWKS server: import (\n    \"crypto/rand\"\n    \"crypto/rsa\"\n    \"encoding/json\"\n    \"net/http/httptest\"\n\n    \"github.com/golang-jwt/jwt/v5\"\n    \"github.com/zoobz-io/rocco/auth0\"\n)\n\nfunc setupMockJWKS(t *testing.T) (*httptest.Server, *rsa.PrivateKey) {\n    t.Helper()\n    privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)\n\n    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        jwks := map[string]any{\n            \"keys\": []map[string]any{{\n                \"kty\": \"RSA\",\n                \"kid\": \"test-key\",\n                \"use\": \"sig\",\n                \"n\":   base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()),\n                \"e\":   \"AQAB\",\n            }},\n        }\n        json.NewEncoder(w).Encode(jwks)\n    }))\n    t.Cleanup(server.Close)\n\n    return server, privateKey\n}\n\nfunc generateTestToken(t *testing.T, key *rsa.PrivateKey, subject string) string {\n    t.Helper()\n    token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{\n        \"sub\": subject,\n        \"iss\": \"https://test.auth0.com/\",\n        \"aud\": \"https://api.test.com\",\n        \"exp\": time.Now().Add(time.Hour).Unix(),\n    })\n    token.Header[\"kid\"] = \"test-key\"\n    signed, _ := token.SignedString(key)\n    return signed\n}",{"id":1318,"title":692,"titles":1319,"content":1320,"level":20},"/v0.1.21/cookbook/authentication#session-based-oauth",[1289],"The rocco/session package provides cookie-based session management with built-in OAuth support. The rocco/oauth package handles the protocol layer.",{"id":1322,"title":1323,"titles":1324,"content":1325,"level":31},"/v0.1.21/cookbook/authentication#complete-oauth-flow","Complete OAuth Flow",[1289,692],"import (\n    \"context\"\n    \"os\"\n\n    \"github.com/zoobz-io/rocco\"\n    \"github.com/zoobz-io/rocco/oauth\"\n    \"github.com/zoobz-io/rocco/session\"\n)\n\nfunc main() {\n    store := session.NewMemoryStore() // Use Redis/database in production\n\n    cfg := session.Config{\n        OAuth: oauth.GitHub(),\n        Store: store,\n        Cookie: session.CookieConfig{\n            SignKey: []byte(os.Getenv(\"SESSION_KEY\")),\n        },\n        Resolve: func(ctx context.Context, tokens *oauth.TokenResponse) (*session.Data, error) {\n            // Call provider API to build session data\n            user, err := fetchGitHubUser(ctx, tokens.AccessToken)\n            if err != nil {\n                return nil, err\n            }\n            return &session.Data{\n                UserID: user.ID,\n                Email:  user.Email,\n                Roles:  user.Roles,\n                Meta:   map[string]any{\"access_token\": tokens.AccessToken},\n            }, nil\n        },\n        RedirectURL: \"/profile\",\n    }\n    cfg.OAuth.ClientID = os.Getenv(\"GITHUB_CLIENT_ID\")\n    cfg.OAuth.ClientSecret = os.Getenv(\"GITHUB_CLIENT_SECRET\")\n    cfg.OAuth.RedirectURI = \"http://localhost:8080/auth/callback\"\n    cfg.OAuth.Scopes = []string{\"read:user\", \"read:org\"}\n\n    login, _ := session.NewLoginHandler(\"/auth/login\", cfg)\n    callback, _ := session.NewCallbackHandler(\"/auth/callback\", cfg)\n    logout, _ := session.NewLogoutHandler(\"/auth/logout\", cfg, \"/\")\n\n    engine := rocco.NewEngine()\n    engine.WithAuthenticator(session.Extractor(store, cfg.Cookie))\n\n    // Public handlers\n    engine.WithHandlers(login, callback, logout)\n\n    // Protected handlers\n    engine.WithHandlers(\n        rocco.GET[rocco.NoBody, ProfileResponse](\"/profile\",\n            func(req *rocco.Request[rocco.NoBody]) (ProfileResponse, error) {\n                return ProfileResponse{\n                    ID:    req.Identity.ID(),\n                    Email: req.Identity.Email(),\n                    Roles: req.Identity.Roles(),\n                }, nil\n            },\n        ).WithAuthentication(),\n    )\n\n    engine.Start(\"\", 8080)\n}\n\ntype ProfileResponse struct {\n    ID    string   `json:\"id\"`\n    Email string   `json:\"email\"`\n    Roles []string `json:\"roles\"`\n} The flow: User visits /auth/login → redirected to GitHubUser authorizes → GitHub redirects to /auth/callbackCallback exchanges code for tokens, calls Resolve, creates session, sets cookie, redirects to /profile/profile reads session cookie, loads identity from store, returns user dataUser visits /auth/logout → session deleted, cookie cleared, redirected to /",{"id":1327,"title":1328,"titles":1329,"content":1330,"level":31},"/v0.1.21/cookbook/authentication#custom-oauth-provider","Custom OAuth Provider",[1289,692],"The oauth package is provider-agnostic. Configure any OAuth 2.0 provider: cfg.OAuth = oauth.Config{\n    Name:         \"my-provider\",\n    AuthURL:      \"https://provider.com/authorize\",\n    TokenURL:     \"https://provider.com/token\",\n    ClientID:     os.Getenv(\"CLIENT_ID\"),\n    ClientSecret: os.Getenv(\"CLIENT_SECRET\"),\n    RedirectURI:  \"https://myapp.com/auth/callback\",\n    Scopes:       []string{\"openid\", \"profile\", \"email\"},\n} For GitHub Enterprise: cfg.OAuth = oauth.GitHubEnterprise(\"https://github.mycompany.com\")",{"id":1332,"title":1333,"titles":1334,"content":1335,"level":31},"/v0.1.21/cookbook/authentication#custom-session-store","Custom Session Store",[1289,692],"Implement session.Store for your backend: type RedisStore struct {\n    client *redis.Client\n    ttl    time.Duration\n}\n\nfunc (s *RedisStore) CreateState(ctx context.Context, state string) error {\n    return s.client.Set(ctx, \"state:\"+state, \"1\", 10*time.Minute).Err()\n}\n\nfunc (s *RedisStore) VerifyState(ctx context.Context, state string) (bool, error) {\n    result, err := s.client.Del(ctx, \"state:\"+state).Result()\n    if err != nil {\n        return false, err\n    }\n    return result > 0, nil\n}\n\nfunc (s *RedisStore) Create(ctx context.Context, id string, data session.Data) error {\n    b, _ := json.Marshal(data)\n    return s.client.Set(ctx, \"session:\"+id, b, s.ttl).Err()\n}\n\nfunc (s *RedisStore) Get(ctx context.Context, id string) (*session.Data, error) {\n    b, err := s.client.Get(ctx, \"session:\"+id).Bytes()\n    if err != nil {\n        return nil, err\n    }\n    var data session.Data\n    return &data, json.Unmarshal(b, &data)\n}\n\nfunc (s *RedisStore) Refresh(ctx context.Context, id string) error {\n    return s.client.Expire(ctx, \"session:\"+id, s.ttl).Err()\n}\n\nfunc (s *RedisStore) Delete(ctx context.Context, id string) error {\n    return s.client.Del(ctx, \"session:\"+id).Err()\n}",{"id":1337,"title":1338,"titles":1339,"content":1340,"level":31},"/v0.1.21/cookbook/authentication#token-refresh","Token Refresh",[1289,692],"Use oauth.Refresh to exchange a refresh token for new tokens: newTokens, err := oauth.Refresh(ctx, cfg.OAuth, oldRefreshToken)\nif err != nil {\n    // Refresh failed - user must re-authenticate\n}",{"id":1342,"title":1343,"titles":1344,"content":1345,"level":31},"/v0.1.21/cookbook/authentication#testing-with-sessions","Testing with Sessions",[1289,692],"For unit tests, use rocco/testing.MockIdentity: func TestProtectedEndpoint(t *testing.T) {\n    mockIdentity := roccotest.NewMockIdentity(\"user-123\").\n        WithEmail(\"dev@example.com\").\n        WithRoles(\"admin\")\n\n    extractor := func(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n        return mockIdentity, nil\n    }\n\n    engine := rocco.NewEngine().WithAuthenticator(extractor)\n    // ...\n} For integration tests, use session.NewMemoryStore with a mock OAuth server: func TestOAuthFlow(t *testing.T) {\n    // Mock OAuth token endpoint\n    mockProvider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        w.Header().Set(\"Content-Type\", \"application/json\")\n        json.NewEncoder(w).Encode(oauth.TokenResponse{\n            AccessToken: \"test-token\",\n            TokenType:   \"Bearer\",\n        })\n    }))\n    defer mockProvider.Close()\n\n    store := session.NewMemoryStore()\n    cfg := session.Config{\n        OAuth: oauth.Config{\n            Name:         \"test\",\n            AuthURL:      \"https://provider.com/auth\",\n            TokenURL:     mockProvider.URL,\n            ClientID:     \"test-id\",\n            ClientSecret: \"test-secret\",\n            RedirectURI:  \"http://localhost/callback\",\n        },\n        Store:  store,\n        Cookie: session.CookieConfig{SignKey: []byte(\"test-key\")},\n        Resolve: func(ctx context.Context, tokens *oauth.TokenResponse) (*session.Data, error) {\n            return &session.Data{UserID: \"user-1\", Email: \"test@example.com\"}, nil\n        },\n        RedirectURL: \"/dashboard\",\n    }\n\n    callback, _ := session.NewCallbackHandler(\"/callback\", cfg)\n    // Pre-create state, then test the callback handler...\n}",{"id":1347,"title":1348,"titles":1349,"content":1350,"level":20},"/v0.1.21/cookbook/authentication#jwt-authentication-manual","JWT Authentication (Manual)",[1289],"For non-Auth0 JWT providers or custom JWT validation.",{"id":1352,"title":1353,"titles":1354,"content":1355,"level":31},"/v0.1.21/cookbook/authentication#identity-implementation","Identity Implementation",[1289,1348],"type JWTIdentity struct {\n    userID   string\n    tenantID string\n    email    string\n    scopes   []string\n    roles    []string\n}\n\nfunc (i *JWTIdentity) ID() string            { return i.userID }\nfunc (i *JWTIdentity) TenantID() string      { return i.tenantID }\nfunc (i *JWTIdentity) Email() string         { return i.email }\nfunc (i *JWTIdentity) Scopes() []string      { return i.scopes }\nfunc (i *JWTIdentity) Roles() []string       { return i.roles }\nfunc (i *JWTIdentity) Stats() map[string]int { return nil }\n\nfunc (i *JWTIdentity) HasScope(scope string) bool {\n    for _, s := range i.scopes {\n        if s == scope {\n            return true\n        }\n    }\n    return false\n}\n\nfunc (i *JWTIdentity) HasRole(role string) bool {\n    for _, r := range i.roles {\n        if r == role {\n            return true\n        }\n    }\n    return false\n}",{"id":1357,"title":1358,"titles":1359,"content":1360,"level":31},"/v0.1.21/cookbook/authentication#identity-extractor","Identity Extractor",[1289,1348],"import (\n    \"context\"\n    \"errors\"\n    \"net/http\"\n    \"strings\"\n\n    \"github.com/golang-jwt/jwt/v5\"\n    \"github.com/zoobz-io/rocco\"\n)\n\nvar jwtSecret = []byte(os.Getenv(\"JWT_SECRET\"))\n\nfunc extractJWTIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // Get Authorization header\n    auth := r.Header.Get(\"Authorization\")\n    if auth == \"\" {\n        return nil, errors.New(\"missing authorization header\")\n    }\n\n    // Validate Bearer format\n    if !strings.HasPrefix(auth, \"Bearer \") {\n        return nil, errors.New(\"invalid authorization format\")\n    }\n\n    tokenString := strings.TrimPrefix(auth, \"Bearer \")\n\n    // Parse and validate token\n    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n        // Validate signing method\n        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n            return nil, errors.New(\"invalid signing method\")\n        }\n        return jwtSecret, nil\n    })\n\n    if err != nil {\n        return nil, err\n    }\n\n    claims, ok := token.Claims.(jwt.MapClaims)\n    if !ok || !token.Valid {\n        return nil, errors.New(\"invalid token\")\n    }\n\n    // Extract claims\n    return &JWTIdentity{\n        userID:   claims[\"sub\"].(string),\n        tenantID: getStringClaim(claims, \"tenant_id\"),\n        email:    getStringClaim(claims, \"email\"),\n        scopes:   getStringSliceClaim(claims, \"scope\"),\n        roles:    getStringSliceClaim(claims, \"roles\"),\n    }, nil\n}\n\nfunc getStringClaim(claims jwt.MapClaims, key string) string {\n    if v, ok := claims[key].(string); ok {\n        return v\n    }\n    return \"\"\n}\n\nfunc getStringSliceClaim(claims jwt.MapClaims, key string) []string {\n    switch v := claims[key].(type) {\n    case []interface{}:\n        result := make([]string, len(v))\n        for i, item := range v {\n            result[i] = item.(string)\n        }\n        return result\n    case string:\n        return strings.Split(v, \" \")\n    }\n    return nil\n}",{"id":1362,"title":1363,"titles":1364,"content":1365,"level":31},"/v0.1.21/cookbook/authentication#login-handler","Login Handler",[1289,1348],"type LoginInput struct {\n    Email    string `json:\"email\" validate:\"required,email\"`\n    Password string `json:\"password\" validate:\"required,min=8\"`\n}\n\ntype LoginOutput struct {\n    Token     string `json:\"token\"`\n    ExpiresAt int64  `json:\"expires_at\"`\n}\n\nvar loginHandler = rocco.NewHandler[LoginInput, LoginOutput](\n    \"login\",\n    \"POST\",\n    \"/auth/login\",\n    func(req *rocco.Request[LoginInput]) (LoginOutput, error) {\n        // Verify credentials\n        user, err := db.GetUserByEmail(req.Body.Email)\n        if err != nil {\n            return LoginOutput{}, rocco.ErrUnauthorized.WithMessage(\"invalid credentials\")\n        }\n\n        if !verifyPassword(req.Body.Password, user.PasswordHash) {\n            return LoginOutput{}, rocco.ErrUnauthorized.WithMessage(\"invalid credentials\")\n        }\n\n        // Generate token\n        expiresAt := time.Now().Add(24 * time.Hour)\n        token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{\n            \"sub\":       user.ID,\n            \"tenant_id\": user.TenantID,\n            \"email\":     user.Email,\n            \"scope\":     strings.Join(user.Scopes, \" \"),\n            \"roles\":     user.Roles,\n            \"exp\":       expiresAt.Unix(),\n            \"iat\":       time.Now().Unix(),\n        })\n\n        tokenString, err := token.SignedString(jwtSecret)\n        if err != nil {\n            return LoginOutput{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return LoginOutput{\n            Token:     tokenString,\n            ExpiresAt: expiresAt.Unix(),\n        }, nil\n    },\n).\n    WithSummary(\"User login\").\n    WithTags(\"auth\").\n    WithErrors(rocco.ErrUnauthorized)",{"id":1367,"title":1368,"titles":1369,"content":1370,"level":31},"/v0.1.21/cookbook/authentication#setup","Setup",[1289,1348],"engine := rocco.NewEngine().WithAuthenticator(extractJWTIdentity)\n\n// Public endpoints\nengine.WithHandlers(loginHandler)\n\n// Protected endpoints\nengine.WithHandlers(\n    rocco.NewHandler[rocco.NoBody, User](\n        \"get-profile\",\n        \"GET\",\n        \"/profile\",\n        getProfile,\n    ).WithAuthentication(),\n)",{"id":1372,"title":1373,"titles":1374,"content":25,"level":20},"/v0.1.21/cookbook/authentication#api-key-authentication","API Key Authentication",[1289],{"id":1376,"title":1353,"titles":1377,"content":1378,"level":31},"/v0.1.21/cookbook/authentication#identity-implementation-1",[1289,1373],"type APIKeyIdentity struct {\n    keyID    string\n    tenantID string\n    name     string\n    scopes   []string\n}\n\nfunc (i *APIKeyIdentity) ID() string            { return i.keyID }\nfunc (i *APIKeyIdentity) TenantID() string      { return i.tenantID }\nfunc (i *APIKeyIdentity) Email() string         { return \"\" }\nfunc (i *APIKeyIdentity) Scopes() []string      { return i.scopes }\nfunc (i *APIKeyIdentity) Roles() []string       { return nil }\nfunc (i *APIKeyIdentity) Stats() map[string]int { return nil }\n\nfunc (i *APIKeyIdentity) HasScope(scope string) bool {\n    for _, s := range i.scopes {\n        if s == scope {\n            return true\n        }\n    }\n    return false\n}\n\nfunc (i *APIKeyIdentity) HasRole(string) bool { return false }",{"id":1380,"title":1358,"titles":1381,"content":1382,"level":31},"/v0.1.21/cookbook/authentication#identity-extractor-1",[1289,1373],"func extractAPIKeyIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // Check header\n    apiKey := r.Header.Get(\"X-API-Key\")\n    if apiKey == \"\" {\n        // Fall back to query param (for webhooks)\n        apiKey = r.URL.Query().Get(\"api_key\")\n    }\n\n    if apiKey == \"\" {\n        return nil, errors.New(\"missing API key\")\n    }\n\n    // Validate key format\n    if !strings.HasPrefix(apiKey, \"sk_\") {\n        return nil, errors.New(\"invalid API key format\")\n    }\n\n    // Look up key (use constant-time comparison in production)\n    keyInfo, err := db.GetAPIKey(hashAPIKey(apiKey))\n    if err != nil {\n        return nil, errors.New(\"invalid API key\")\n    }\n\n    // Check if key is active\n    if !keyInfo.Active {\n        return nil, errors.New(\"API key disabled\")\n    }\n\n    // Check expiration\n    if keyInfo.ExpiresAt != nil && keyInfo.ExpiresAt.Before(time.Now()) {\n        return nil, errors.New(\"API key expired\")\n    }\n\n    return &APIKeyIdentity{\n        keyID:    keyInfo.ID,\n        tenantID: keyInfo.TenantID,\n        name:     keyInfo.Name,\n        scopes:   keyInfo.Scopes,\n    }, nil\n}",{"id":1384,"title":1385,"titles":1386,"content":1387,"level":31},"/v0.1.21/cookbook/authentication#api-key-management","API Key Management",[1289,1373],"type CreateAPIKeyInput struct {\n    Name   string   `json:\"name\" validate:\"required,min=1,max=100\"`\n    Scopes []string `json:\"scopes\" validate:\"required,min=1\"`\n}\n\ntype APIKeyOutput struct {\n    ID        string    `json:\"id\"`\n    Key       string    `json:\"key,omitempty\"` // Only returned on creation\n    Name      string    `json:\"name\"`\n    Scopes    []string  `json:\"scopes\"`\n    CreatedAt time.Time `json:\"created_at\"`\n}\n\nvar createAPIKey = rocco.NewHandler[CreateAPIKeyInput, APIKeyOutput](\n    \"create-api-key\",\n    \"POST\",\n    \"/api-keys\",\n    func(req *rocco.Request[CreateAPIKeyInput]) (APIKeyOutput, error) {\n        // Generate key\n        key := \"sk_\" + generateSecureRandom(32)\n        keyHash := hashAPIKey(key)\n\n        keyInfo := &APIKey{\n            ID:        generateID(),\n            KeyHash:   keyHash,\n            TenantID:  req.Identity.TenantID(),\n            Name:      req.Body.Name,\n            Scopes:    req.Body.Scopes,\n            CreatedAt: time.Now(),\n        }\n\n        if err := db.CreateAPIKey(keyInfo); err != nil {\n            return APIKeyOutput{}, rocco.ErrInternalServer.WithCause(err)\n        }\n\n        return APIKeyOutput{\n            ID:        keyInfo.ID,\n            Key:       key, // Only returned once!\n            Name:      keyInfo.Name,\n            Scopes:    keyInfo.Scopes,\n            CreatedAt: keyInfo.CreatedAt,\n        }, nil\n    },\n).\n    WithAuthentication().\n    WithScopes(\"api-keys:write\").\n    WithSuccessStatus(201)",{"id":1389,"title":1390,"titles":1391,"content":1392,"level":20},"/v0.1.21/cookbook/authentication#session-authentication","Session Authentication",[1289],"For session-based authentication, use the rocco/session package. It provides a session.Identity implementation, cookie management with HMAC-SHA256 signing, and a pluggable session.Store interface. See the Session-Based OAuth section above for complete setup. The session.Extractor function handles cookie reading, signature verification, and session lookup automatically. For custom session needs beyond OAuth (e.g., password-based login), use the store and cookie primitives directly: store := session.NewMemoryStore()\ncookie := session.CookieConfig{SignKey: []byte(os.Getenv(\"SESSION_KEY\"))}\n\n// In your login handler, create a session manually:\nsessionID, _ := generateID()\nstore.Create(ctx, sessionID, session.Data{\n    UserID: user.ID,\n    Email:  user.Email,\n    Roles:  user.Roles,\n})\n\n// Use the same extractor for identity extraction:\nengine.WithAuthenticator(session.Extractor(store, cookie))",{"id":1394,"title":1395,"titles":1396,"content":1397,"level":20},"/v0.1.21/cookbook/authentication#multi-auth-support","Multi-Auth Support",[1289],"Support multiple authentication methods: func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // Try JWT first\n    if auth := r.Header.Get(\"Authorization\"); strings.HasPrefix(auth, \"Bearer \") {\n        return extractJWTIdentity(ctx, r)\n    }\n\n    // Try API key\n    if apiKey := r.Header.Get(\"X-API-Key\"); apiKey != \"\" {\n        return extractAPIKeyIdentity(ctx, r)\n    }\n\n    // Try session cookie\n    if _, err := r.Cookie(\"sid\"); err == nil {\n        return session.Extractor(store, cookieCfg)(ctx, r)\n    }\n\n    return nil, errors.New(\"no authentication provided\")\n}",{"id":1399,"title":1400,"titles":1401,"content":1402,"level":20},"/v0.1.21/cookbook/authentication#usage-limits-with-stats","Usage Limits with Stats",[1289],"type PlanBasedIdentity struct {\n    userID   string\n    tenantID string\n    email    string\n    plan     string // \"free\", \"pro\", \"enterprise\"\n    scopes   []string\n    roles    []string\n    stats    map[string]int\n}\n\nfunc (i *PlanBasedIdentity) ID() string            { return i.userID }\nfunc (i *PlanBasedIdentity) TenantID() string      { return i.tenantID }\nfunc (i *PlanBasedIdentity) Email() string         { return i.email }\nfunc (i *PlanBasedIdentity) Scopes() []string      { return i.scopes }\nfunc (i *PlanBasedIdentity) Roles() []string       { return i.roles }\nfunc (i *PlanBasedIdentity) HasScope(s string) bool {\n    for _, scope := range i.scopes {\n        if scope == s {\n            return true\n        }\n    }\n    return false\n}\nfunc (i *PlanBasedIdentity) HasRole(r string) bool {\n    for _, role := range i.roles {\n        if role == r {\n            return true\n        }\n    }\n    return false\n}\nfunc (i *PlanBasedIdentity) Stats() map[string]int { return i.stats }\n\n// Handler with usage limits\nvar createResource = rocco.NewHandler[Input, Output](\n    \"create-resource\",\n    \"POST\",\n    \"/resources\",\n    createResourceHandler,\n).\n    WithAuthentication().\n    WithUsageLimit(\"resources_created_today\", func(id rocco.Identity) int {\n        // Dynamic limits based on plan\n        planID := id.(*PlanBasedIdentity).plan\n        switch planID {\n        case \"enterprise\":\n            return 10000\n        case \"pro\":\n            return 1000\n        default:\n            return 100\n        }\n    })",{"id":1404,"title":1405,"titles":1406,"content":25,"level":20},"/v0.1.21/cookbook/authentication#security-best-practices","Security Best Practices",[1289],{"id":1408,"title":1409,"titles":1410,"content":1411,"level":31},"/v0.1.21/cookbook/authentication#_1-use-https","1. Use HTTPS",[1289,1405],"Always use HTTPS in production. Rocco doesn't handle TLS - use a reverse proxy.",{"id":1413,"title":1414,"titles":1415,"content":1416,"level":31},"/v0.1.21/cookbook/authentication#_2-short-token-lifetimes","2. Short Token Lifetimes",[1289,1405],"// Access tokens: short lived\naccessExpiry := time.Now().Add(15 * time.Minute)\n\n// Refresh tokens: longer, but rotated\nrefreshExpiry := time.Now().Add(7 * 24 * time.Hour)",{"id":1418,"title":1419,"titles":1420,"content":1421,"level":31},"/v0.1.21/cookbook/authentication#_3-log-security-events","3. Log Security Events",[1289,1405],"capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {\n    path, _ := rocco.PathKey.From(e)\n    err, _ := rocco.ErrorKey.From(e)\n    securityLog.Warn(\"auth_failed\", \"path\", path, \"error\", err)\n})",{"id":1423,"title":1424,"titles":1425,"content":1426,"level":31},"/v0.1.21/cookbook/authentication#_4-rate-limit-auth-endpoints","4. Rate Limit Auth Endpoints",[1289,1405],"loginHandler.WithMiddleware(rateLimiter(5, time.Minute))",{"id":1428,"title":1429,"titles":1430,"content":1431,"level":31},"/v0.1.21/cookbook/authentication#_5-hash-api-keys","5. Hash API Keys",[1289,1405],"func hashAPIKey(key string) string {\n    hash := sha256.Sum256([]byte(key))\n    return hex.EncodeToString(hash[:])\n}",{"id":1433,"title":98,"titles":1434,"content":1435,"level":20},"/v0.1.21/cookbook/authentication#see-also",[1289],"Authentication Guide - Detailed auth patternsBest Practices - Security recommendationsEvents Reference - Auth events html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .scyPU, html code.shiki .scyPU{--shiki-default:var(--shiki-placeholder)}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":1437,"title":78,"titles":1438,"content":1439,"level":9},"/v0.1.21/cookbook/observability",[],"Logging, metrics, and tracing with capitan",{"id":1441,"title":1442,"titles":1443,"content":1444,"level":9},"/v0.1.21/cookbook/observability#observability-cookbook","Observability Cookbook",[],"Implement logging, metrics, and tracing using rocco's event system.",{"id":1446,"title":1447,"titles":1448,"content":1449,"level":20},"/v0.1.21/cookbook/observability#event-system-overview","Event System Overview",[1442],"Rocco emits lifecycle events via capitan. You hook into these events to build observability. import (\n    \"context\"\n    \"github.com/zoobz-io/capitan\"\n    \"github.com/zoobz-io/rocco\"\n)\n\n// Hook specific events\ncapitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n    // Handle request completed event\n})\n\n// Observe all events\ncapitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    // Handle any event\n})",{"id":1451,"title":1452,"titles":1453,"content":25,"level":20},"/v0.1.21/cookbook/observability#structured-logging","Structured Logging",[1442],{"id":1455,"title":1456,"titles":1457,"content":1458,"level":31},"/v0.1.21/cookbook/observability#request-logging","Request Logging",[1442,1452],"import (\n    \"log/slog\"\n    \"os\"\n)\n\nvar logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))\n\nfunc setupRequestLogging() {\n    // Log successful requests\n    capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n        duration, _ := rocco.DurationMsKey.From(e)\n        handler, _ := rocco.HandlerNameKey.From(e)\n\n        logger.Info(\"request\",\n            \"method\", method,\n            \"path\", path,\n            \"status\", status,\n            \"duration_ms\", duration,\n            \"handler\", handler,\n        )\n    })\n\n    // Log failed requests with error details\n    capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n        duration, _ := rocco.DurationMsKey.From(e)\n        handler, _ := rocco.HandlerNameKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n\n        logger.Error(\"request_failed\",\n            \"method\", method,\n            \"path\", path,\n            \"status\", status,\n            \"duration_ms\", duration,\n            \"handler\", handler,\n            \"error\", errMsg,\n        )\n    })\n}",{"id":1460,"title":1461,"titles":1462,"content":1463,"level":31},"/v0.1.21/cookbook/observability#security-logging","Security Logging",[1442,1452],"func setupSecurityLogging() {\n    securityLogger := slog.New(slog.NewJSONHandler(\n        os.Stdout,\n        &slog.HandlerOptions{Level: slog.LevelWarn},\n    ))\n\n    // Authentication failures\n    capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n\n        securityLogger.Warn(\"auth_failed\",\n            \"method\", method,\n            \"path\", path,\n            \"error\", errMsg,\n        )\n    })\n\n    // Authorization denials\n    capitan.Hook(rocco.AuthorizationScopeDenied, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        identityID, _ := rocco.IdentityIDKey.From(e)\n        required, _ := rocco.RequiredScopesKey.From(e)\n\n        securityLogger.Warn(\"authz_denied\",\n            \"method\", method,\n            \"path\", path,\n            \"identity\", identityID,\n            \"required_scopes\", required,\n        )\n    })\n\n    // Rate limit exceeded\n    capitan.Hook(rocco.RateLimitExceeded, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        identityID, _ := rocco.IdentityIDKey.From(e)\n        limitKey, _ := rocco.LimitKeyKey.From(e)\n        current, _ := rocco.CurrentValueKey.From(e)\n        threshold, _ := rocco.ThresholdKey.From(e)\n\n        securityLogger.Warn(\"rate_limit_exceeded\",\n            \"method\", method,\n            \"path\", path,\n            \"identity\", identityID,\n            \"limit_key\", limitKey,\n            \"current\", current,\n            \"threshold\", threshold,\n        )\n    })\n}",{"id":1465,"title":1466,"titles":1467,"content":1468,"level":31},"/v0.1.21/cookbook/observability#error-logging","Error Logging",[1442,1452],"func setupErrorLogging() {\n    // Handler errors (unexpected)\n    capitan.Hook(rocco.HandlerError, func(ctx context.Context, e *capitan.Event) {\n        handler, _ := rocco.HandlerNameKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n\n        logger.Error(\"handler_error\",\n            \"handler\", handler,\n            \"error\", errMsg,\n        )\n    })\n\n    // Undeclared sentinel errors (programming error)\n    capitan.Hook(rocco.HandlerUndeclaredSentinel, func(ctx context.Context, e *capitan.Event) {\n        handler, _ := rocco.HandlerNameKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n\n        logger.Error(\"undeclared_sentinel\",\n            \"handler\", handler,\n            \"error\", errMsg,\n            \"would_be_status\", status,\n        )\n    })\n\n    // Validation failures\n    capitan.Hook(rocco.RequestValidationInputFailed, func(ctx context.Context, e *capitan.Event) {\n        handler, _ := rocco.HandlerNameKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n\n        logger.Debug(\"validation_failed\",\n            \"handler\", handler,\n            \"error\", errMsg,\n        )\n    })\n}",{"id":1470,"title":1076,"titles":1471,"content":25,"level":20},"/v0.1.21/cookbook/observability#metrics",[1442],{"id":1473,"title":1474,"titles":1475,"content":1476,"level":31},"/v0.1.21/cookbook/observability#prometheus-metrics","Prometheus Metrics",[1442,1076],"import (\n    \"github.com/prometheus/client_golang/prometheus\"\n    \"github.com/prometheus/client_golang/prometheus/promauto\"\n)\n\nvar (\n    requestsTotal = promauto.NewCounterVec(\n        prometheus.CounterOpts{\n            Name: \"http_requests_total\",\n            Help: \"Total number of HTTP requests\",\n        },\n        []string{\"method\", \"path\", \"status\", \"handler\"},\n    )\n\n    requestDuration = promauto.NewHistogramVec(\n        prometheus.HistogramOpts{\n            Name:    \"http_request_duration_seconds\",\n            Help:    \"HTTP request duration in seconds\",\n            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},\n        },\n        []string{\"method\", \"path\", \"handler\"},\n    )\n\n    errorsTotal = promauto.NewCounterVec(\n        prometheus.CounterOpts{\n            Name: \"http_errors_total\",\n            Help: \"Total number of HTTP errors\",\n        },\n        []string{\"method\", \"path\", \"handler\", \"error_code\"},\n    )\n)\n\nfunc setupPrometheusMetrics() {\n    // Request metrics\n    capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n        duration, _ := rocco.DurationMsKey.From(e)\n        handler, _ := rocco.HandlerNameKey.From(e)\n\n        requestsTotal.WithLabelValues(method, path, fmt.Sprint(status), handler).Inc()\n        requestDuration.WithLabelValues(method, path, handler).Observe(float64(duration) / 1000)\n    })\n\n    capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n        duration, _ := rocco.DurationMsKey.From(e)\n        handler, _ := rocco.HandlerNameKey.From(e)\n\n        requestsTotal.WithLabelValues(method, path, fmt.Sprint(status), handler).Inc()\n        requestDuration.WithLabelValues(method, path, handler).Observe(float64(duration) / 1000)\n        errorsTotal.WithLabelValues(method, path, handler, fmt.Sprint(status)).Inc()\n    })\n}",{"id":1478,"title":1479,"titles":1480,"content":1481,"level":31},"/v0.1.21/cookbook/observability#custom-metrics","Custom Metrics",[1442,1076],"var (\n    authFailures = promauto.NewCounterVec(\n        prometheus.CounterOpts{\n            Name: \"auth_failures_total\",\n            Help: \"Total authentication failures\",\n        },\n        []string{\"path\"},\n    )\n\n    rateLimitHits = promauto.NewCounterVec(\n        prometheus.CounterOpts{\n            Name: \"rate_limit_hits_total\",\n            Help: \"Total rate limit hits\",\n        },\n        []string{\"identity\", \"limit_key\"},\n    )\n)\n\nfunc setupCustomMetrics() {\n    capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {\n        path, _ := rocco.PathKey.From(e)\n        authFailures.WithLabelValues(path).Inc()\n    })\n\n    capitan.Hook(rocco.RateLimitExceeded, func(ctx context.Context, e *capitan.Event) {\n        identityID, _ := rocco.IdentityIDKey.From(e)\n        limitKey, _ := rocco.LimitKeyKey.From(e)\n        rateLimitHits.WithLabelValues(identityID, limitKey).Inc()\n    })\n}",{"id":1483,"title":1484,"titles":1485,"content":1486,"level":20},"/v0.1.21/cookbook/observability#opentelemetry-tracing","OpenTelemetry Tracing",[1442],"import (\n    \"go.opentelemetry.io/otel\"\n    \"go.opentelemetry.io/otel/attribute\"\n    \"go.opentelemetry.io/otel/trace\"\n)\n\nvar tracer = otel.Tracer(\"rocco\")\n\nfunc setupTracing() {\n    capitan.Hook(rocco.RequestReceived, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        handler, _ := rocco.HandlerNameKey.From(e)\n\n        _, span := tracer.Start(ctx, handler,\n            trace.WithSpanKind(trace.SpanKindServer),\n            trace.WithAttributes(\n                attribute.String(\"http.method\", method),\n                attribute.String(\"http.route\", path),\n            ),\n        )\n        defer span.End()\n    })\n\n    capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n        span := trace.SpanFromContext(ctx)\n        status, _ := rocco.StatusCodeKey.From(e)\n        span.SetAttributes(attribute.Int(\"http.status_code\", status))\n    })\n\n    capitan.Hook(rocco.RequestFailed, func(ctx context.Context, e *capitan.Event) {\n        span := trace.SpanFromContext(ctx)\n        status, _ := rocco.StatusCodeKey.From(e)\n        errMsg, _ := rocco.ErrorKey.From(e)\n\n        span.SetAttributes(\n            attribute.Int(\"http.status_code\", status),\n            attribute.String(\"error\", errMsg),\n        )\n        span.RecordError(errors.New(errMsg))\n    })\n}",{"id":1488,"title":1489,"titles":1490,"content":1491,"level":20},"/v0.1.21/cookbook/observability#global-observer","Global Observer",[1442],"Handle all events with a single observer: func setupGlobalObserver() {\n    capitan.Observe(func(ctx context.Context, e *capitan.Event) {\n        switch e.Signal {\n        // Request lifecycle\n        case rocco.RequestReceived:\n            handleRequestReceived(ctx, e)\n        case rocco.RequestCompleted:\n            handleRequestCompleted(ctx, e)\n        case rocco.RequestFailed:\n            handleRequestFailed(ctx, e)\n\n        // Errors\n        case rocco.HandlerError:\n            handleHandlerError(ctx, e)\n        case rocco.HandlerUndeclaredSentinel:\n            handleUndeclaredSentinel(ctx, e)\n\n        // Security\n        case rocco.AuthenticationFailed:\n            handleAuthFailed(ctx, e)\n        case rocco.AuthorizationScopeDenied, rocco.AuthorizationRoleDenied:\n            handleAuthzDenied(ctx, e)\n        case rocco.RateLimitExceeded:\n            handleRateLimit(ctx, e)\n\n        // Server lifecycle\n        case rocco.EngineStarting:\n            handleEngineStarting(ctx, e)\n        case rocco.EngineShutdownComplete:\n            handleEngineShutdown(ctx, e)\n        }\n    })\n}",{"id":1493,"title":1494,"titles":1495,"content":1496,"level":20},"/v0.1.21/cookbook/observability#complete-setup","Complete Setup",[1442],"package main\n\nimport (\n    \"github.com/zoobz-io/capitan\"\n    \"github.com/zoobz-io/rocco\"\n)\n\nfunc main() {\n    // Setup observability before creating engine\n    setupRequestLogging()\n    setupSecurityLogging()\n    setupErrorLogging()\n    setupPrometheusMetrics()\n\n    // Create engine\n    engine := rocco.NewEngine().WithAuthenticator(extractIdentity)\n\n    // Register handlers\n    engine.WithHandlers(handlers...)\n\n    // Start server\n    engine.Start(rocco.HostAll, 8080)\n}",{"id":1498,"title":1499,"titles":1500,"content":1501,"level":20},"/v0.1.21/cookbook/observability#testing-observability","Testing Observability",[1442],"import \"github.com/zoobz-io/capitan\"\n\nfunc TestMetricsEmission(t *testing.T) {\n    // Configure sync mode for testing\n    capitan.Configure(capitan.WithSyncMode())\n\n    var requestReceived bool\n    listener := capitan.Hook(rocco.RequestReceived, func(_ context.Context, e *capitan.Event) {\n        requestReceived = true\n    })\n    defer listener.Close()\n\n    // Make request\n    engine := rtesting.TestEngine()\n    engine.WithHandlers(handler)\n    rtesting.ServeRequest(engine, \"GET\", \"/test\", nil)\n\n    if !requestReceived {\n        t.Error(\"RequestReceived event not emitted\")\n    }\n}",{"id":1503,"title":98,"titles":1504,"content":1505,"level":20},"/v0.1.21/cookbook/observability#see-also",[1442],"Events Reference - Complete event listBest Practices - Production patternscapitan Documentation - Event system docs html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"id":1507,"title":1508,"titles":1509,"content":1510,"level":9},"/v0.1.21/cookbook/realtime","Real-time Patterns",[],"Recipes for building real-time features with SSE",{"id":1512,"title":1513,"titles":1514,"content":1515,"level":9},"/v0.1.21/cookbook/realtime#real-time-patterns-cookbook","Real-time Patterns Cookbook",[],"Practical recipes for building real-time features with Server-Sent Events.",{"id":1517,"title":1518,"titles":1519,"content":1520,"level":20},"/v0.1.21/cookbook/realtime#price-ticker","Price Ticker",[1513],"Stream financial data updates to clients: type PriceUpdate struct {\n    Symbol    string    `json:\"symbol\"`\n    Price     float64   `json:\"price\"`\n    Change    float64   `json:\"change\"`\n    Timestamp time.Time `json:\"timestamp\"`\n}\n\ntype TickerConfig struct {\n    Symbols []string `json:\"symbols\" validate:\"required,min=1,max=10\"`\n}\n\nfunc NewPriceTickerHandler(priceService *PriceService) *rocco.StreamHandler[TickerConfig, PriceUpdate] {\n    return rocco.NewStreamHandler[TickerConfig, PriceUpdate](\n        \"price-ticker\",\n        http.MethodPost,\n        \"/prices/stream\",\n        func(req *rocco.Request[TickerConfig], stream rocco.Stream[PriceUpdate]) error {\n            // Subscribe to price updates for requested symbols\n            updates := priceService.Subscribe(req.Body.Symbols)\n            defer priceService.Unsubscribe(updates)\n\n            ticker := time.NewTicker(30 * time.Second)\n            defer ticker.Stop()\n\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                case \u003C-ticker.C:\n                    stream.SendComment(\"ping\")\n                case update := \u003C-updates:\n                    if err := stream.Send(update); err != nil {\n                        return err\n                    }\n                }\n            }\n        },\n    ).\n        WithSummary(\"Stream price updates\").\n        WithDescription(\"Streams real-time price updates for specified symbols\").\n        WithTags(\"prices\", \"streaming\")\n}",{"id":1522,"title":1523,"titles":1524,"content":1525,"level":20},"/v0.1.21/cookbook/realtime#user-notifications","User Notifications",[1513],"Stream notifications for authenticated users: type Notification struct {\n    ID        string    `json:\"id\"`\n    Type      string    `json:\"type\"`\n    Title     string    `json:\"title\"`\n    Body      string    `json:\"body\"`\n    Read      bool      `json:\"read\"`\n    CreatedAt time.Time `json:\"created_at\"`\n}\n\nfunc NewNotificationHandler(notifService *NotificationService) *rocco.StreamHandler[rocco.NoBody, Notification] {\n    return rocco.NewStreamHandler[rocco.NoBody, Notification](\n        \"notifications\",\n        http.MethodGet,\n        \"/notifications/stream\",\n        func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Notification]) error {\n            userID := req.Identity.ID()\n\n            // Send unread notifications first\n            unread, err := notifService.GetUnread(userID)\n            if err != nil {\n                return err\n            }\n            for _, n := range unread {\n                if err := stream.SendEvent(\"unread\", n); err != nil {\n                    return err\n                }\n            }\n\n            // Stream new notifications\n            newNotifs := notifService.Subscribe(userID)\n            defer notifService.Unsubscribe(userID, newNotifs)\n\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                case notif := \u003C-newNotifs:\n                    if err := stream.SendEvent(\"new\", notif); err != nil {\n                        return err\n                    }\n                }\n            }\n        },\n    ).\n        WithSummary(\"Stream notifications\").\n        WithAuthentication().\n        WithScopes(\"notifications:read\")\n}",{"id":1527,"title":1528,"titles":1529,"content":1530,"level":20},"/v0.1.21/cookbook/realtime#progress-updates","Progress Updates",[1513],"Stream progress for long-running tasks: type ProgressUpdate struct {\n    TaskID     string  `json:\"task_id\"`\n    Status     string  `json:\"status\"` // pending, running, completed, failed\n    Progress   float64 `json:\"progress\"` // 0.0 to 1.0\n    Message    string  `json:\"message,omitempty\"`\n    Error      string  `json:\"error,omitempty\"`\n}\n\nfunc NewProgressHandler(taskService *TaskService) *rocco.StreamHandler[rocco.NoBody, ProgressUpdate] {\n    return rocco.NewStreamHandler[rocco.NoBody, ProgressUpdate](\n        \"task-progress\",\n        http.MethodGet,\n        \"/tasks/{taskId}/progress\",\n        func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[ProgressUpdate]) error {\n            taskID := req.Params.Path[\"taskId\"]\n\n            updates := taskService.SubscribeProgress(taskID)\n            defer taskService.UnsubscribeProgress(taskID, updates)\n\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                case update, ok := \u003C-updates:\n                    if !ok {\n                        // Channel closed, task complete\n                        return nil\n                    }\n                    if err := stream.Send(update); err != nil {\n                        return err\n                    }\n                    if update.Status == \"completed\" || update.Status == \"failed\" {\n                        return nil\n                    }\n                }\n            }\n        },\n    ).\n        WithSummary(\"Stream task progress\").\n        WithPathParams(\"taskId\").\n        WithAuthentication()\n}",{"id":1532,"title":1533,"titles":1534,"content":1535,"level":20},"/v0.1.21/cookbook/realtime#chat-pub-sub-pattern","Chat / Pub-Sub Pattern",[1513],"Stream messages for a channel: type ChatMessage struct {\n    ID        string    `json:\"id\"`\n    Channel   string    `json:\"channel\"`\n    UserID    string    `json:\"user_id\"`\n    Username  string    `json:\"username\"`\n    Content   string    `json:\"content\"`\n    Timestamp time.Time `json:\"timestamp\"`\n}\n\ntype ChannelEvent struct {\n    Type    string `json:\"type\"` // message, join, leave, typing\n    Payload any    `json:\"payload\"`\n}\n\nfunc NewChatHandler(chatService *ChatService) *rocco.StreamHandler[rocco.NoBody, ChannelEvent] {\n    return rocco.NewStreamHandler[rocco.NoBody, ChannelEvent](\n        \"chat-stream\",\n        http.MethodGet,\n        \"/channels/{channel}/stream\",\n        func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[ChannelEvent]) error {\n            channel := req.Params.Path[\"channel\"]\n            userID := req.Identity.ID()\n\n            // Join channel\n            sub, err := chatService.Join(channel, userID)\n            if err != nil {\n                return rocco.ErrForbidden.WithMessage(\"cannot join channel\")\n            }\n            defer chatService.Leave(channel, userID)\n\n            // Notify others of join\n            stream.SendEvent(\"system\", ChannelEvent{\n                Type:    \"joined\",\n                Payload: map[string]string{\"channel\": channel},\n            })\n\n            keepAlive := time.NewTicker(30 * time.Second)\n            defer keepAlive.Stop()\n\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                case \u003C-keepAlive.C:\n                    stream.SendComment(\"ping\")\n                case event := \u003C-sub.Events:\n                    if err := stream.SendEvent(event.Type, event); err != nil {\n                        return err\n                    }\n                }\n            }\n        },\n    ).\n        WithSummary(\"Stream channel events\").\n        WithPathParams(\"channel\").\n        WithAuthentication()\n}",{"id":1537,"title":1538,"titles":1539,"content":1540,"level":20},"/v0.1.21/cookbook/realtime#dashboard-metrics","Dashboard Metrics",[1513],"Stream live metrics for dashboards: type DashboardMetrics struct {\n    Timestamp       time.Time `json:\"timestamp\"`\n    ActiveUsers     int       `json:\"active_users\"`\n    RequestsPerSec  float64   `json:\"requests_per_sec\"`\n    ErrorRate       float64   `json:\"error_rate\"`\n    AvgLatencyMs    float64   `json:\"avg_latency_ms\"`\n    CPUUsage        float64   `json:\"cpu_usage\"`\n    MemoryUsage     float64   `json:\"memory_usage\"`\n}\n\nfunc NewMetricsHandler(metricsService *MetricsService) *rocco.StreamHandler[rocco.NoBody, DashboardMetrics] {\n    return rocco.NewStreamHandler[rocco.NoBody, DashboardMetrics](\n        \"dashboard-metrics\",\n        http.MethodGet,\n        \"/admin/metrics/stream\",\n        func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[DashboardMetrics]) error {\n            ticker := time.NewTicker(time.Second)\n            defer ticker.Stop()\n\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                case \u003C-ticker.C:\n                    metrics := metricsService.Current()\n                    if err := stream.Send(metrics); err != nil {\n                        return err\n                    }\n                }\n            }\n        },\n    ).\n        WithSummary(\"Stream dashboard metrics\").\n        WithAuthentication().\n        WithRoles(\"admin\")\n}",{"id":1542,"title":1543,"titles":1544,"content":1545,"level":20},"/v0.1.21/cookbook/realtime#connection-management-pattern","Connection Management Pattern",[1513],"Manage multiple concurrent connections with limits: type ConnectionManager struct {\n    mu          sync.RWMutex\n    connections map[string]int // userID -> connection count\n    maxPerUser  int\n}\n\nfunc NewConnectionManager(maxPerUser int) *ConnectionManager {\n    return &ConnectionManager{\n        connections: make(map[string]int),\n        maxPerUser:  maxPerUser,\n    }\n}\n\nfunc (cm *ConnectionManager) Acquire(userID string) error {\n    cm.mu.Lock()\n    defer cm.mu.Unlock()\n\n    if cm.connections[userID] >= cm.maxPerUser {\n        return errors.New(\"max connections exceeded\")\n    }\n    cm.connections[userID]++\n    return nil\n}\n\nfunc (cm *ConnectionManager) Release(userID string) {\n    cm.mu.Lock()\n    defer cm.mu.Unlock()\n\n    cm.connections[userID]--\n    if cm.connections[userID] \u003C= 0 {\n        delete(cm.connections, userID)\n    }\n}\n\n// Usage in handler\nfunc NewManagedStreamHandler(cm *ConnectionManager) *rocco.StreamHandler[rocco.NoBody, Event] {\n    return rocco.NewStreamHandler[rocco.NoBody, Event](\n        \"managed-stream\",\n        http.MethodGet,\n        \"/events\",\n        func(req *rocco.Request[rocco.NoBody], stream rocco.Stream[Event]) error {\n            userID := req.Identity.ID()\n\n            if err := cm.Acquire(userID); err != nil {\n                return rocco.ErrTooManyRequests.WithMessage(\"too many connections\")\n            }\n            defer cm.Release(userID)\n\n            // ... stream logic\n            for {\n                select {\n                case \u003C-stream.Done():\n                    return nil\n                // ...\n                }\n            }\n        },\n    ).WithAuthentication()\n}",{"id":1547,"title":1548,"titles":1549,"content":25,"level":20},"/v0.1.21/cookbook/realtime#client-side-usage","Client-Side Usage",[1513],{"id":1551,"title":1552,"titles":1553,"content":1554,"level":31},"/v0.1.21/cookbook/realtime#javascript-eventsource","JavaScript EventSource",[1513,1548],"// Basic usage\nconst source = new EventSource('/events');\n\nsource.onmessage = (event) => {\n    const data = JSON.parse(event.data);\n    console.log('Received:', data);\n};\n\nsource.onerror = (error) => {\n    console.error('SSE error:', error);\n};\n\n// Named events\nsource.addEventListener('price', (event) => {\n    const price = JSON.parse(event.data);\n    updatePriceDisplay(price);\n});\n\nsource.addEventListener('notification', (event) => {\n    const notif = JSON.parse(event.data);\n    showNotification(notif);\n});\n\n// With authentication (POST not supported by EventSource)\n// Use fetch with ReadableStream instead\nasync function streamWithAuth(url, token) {\n    const response = await fetch(url, {\n        headers: { 'Authorization': `Bearer ${token}` }\n    });\n\n    const reader = response.body.getReader();\n    const decoder = new TextDecoder();\n\n    while (true) {\n        const { done, value } = await reader.read();\n        if (done) break;\n\n        const text = decoder.decode(value);\n        // Parse SSE format\n        const lines = text.split('\\n');\n        for (const line of lines) {\n            if (line.startsWith('data: ')) {\n                const data = JSON.parse(line.slice(6));\n                handleEvent(data);\n            }\n        }\n    }\n}",{"id":1556,"title":98,"titles":1557,"content":1558,"level":20},"/v0.1.21/cookbook/realtime#see-also",[1513],"Streaming Guide - Complete SSE documentationAuthentication Guide - Securing streamsEvents Reference - Stream lifecycle events html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .skxcq, html code.shiki .skxcq{--shiki-default:var(--shiki-builtin)}",{"id":1560,"title":1561,"titles":1562,"content":1563,"level":9},"/v0.1.21/reference/api","API Reference",[],"Complete API documentation for rocco",{"id":1565,"title":1561,"titles":1566,"content":1567,"level":9},"/v0.1.21/reference/api#api-reference",[],"Complete reference for all rocco types and functions.",{"id":1569,"title":1570,"titles":1571,"content":25,"level":20},"/v0.1.21/reference/api#constants","Constants",[1561],{"id":1573,"title":1574,"titles":1575,"content":1576,"level":31},"/v0.1.21/reference/api#host-constants","Host Constants",[1561,1570],"Constants for common host values used with Engine.Start(). ConstantValueDescriptionHostAll\"\"Bind to all interfaces (0.0.0.0)HostLocal\"localhost\"Bind to loopback (localhost)HostLoopback\"127.0.0.1\"Bind to loopback (127.0.0.1) engine.Start(rocco.HostAll, 8080)      // All interfaces\nengine.Start(rocco.HostLocal, 8080)    // Localhost only\nengine.Start(rocco.HostLoopback, 8080) // 127.0.0.1 only",{"id":1578,"title":160,"titles":1579,"content":25,"level":20},"/v0.1.21/reference/api#engine",[1561],{"id":1581,"title":1582,"titles":1583,"content":1584,"level":31},"/v0.1.21/reference/api#newengine","NewEngine",[1561,160],"func NewEngine() *Engine Creates a new Engine instance.",{"id":1586,"title":1587,"titles":1588,"content":25,"level":31},"/v0.1.21/reference/api#engine-methods","Engine Methods",[1561,160],{"id":1590,"title":1591,"titles":1592,"content":1593,"level":844},"/v0.1.21/reference/api#withauthenticator","WithAuthenticator",[1561,160,1587],"func (e *Engine) WithAuthenticator(extractor func(context.Context, *http.Request) (Identity, error)) *Engine Sets the identity extraction function for authenticated handlers. Returns engine for chaining.",{"id":1595,"title":1596,"titles":1597,"content":1598,"level":844},"/v0.1.21/reference/api#withmiddleware","WithMiddleware",[1561,160,1587],"func (e *Engine) WithMiddleware(middleware ...func(http.Handler) http.Handler) *Engine Adds global middleware. Returns engine for chaining.",{"id":1600,"title":1601,"titles":1602,"content":1603,"level":844},"/v0.1.21/reference/api#withhandlers","WithHandlers",[1561,160,1587],"func (e *Engine) WithHandlers(handlers ...Endpoint) *Engine Registers handlers with the engine. Returns engine for chaining.",{"id":1605,"title":1606,"titles":1607,"content":1608,"level":844},"/v0.1.21/reference/api#withspec","WithSpec",[1561,160,1587],"func (e *Engine) WithSpec(spec *EngineSpec) *Engine Sets the OpenAPI specification configuration.",{"id":1610,"title":1611,"titles":1612,"content":1613,"level":844},"/v0.1.21/reference/api#withopenapiinfo","WithOpenAPIInfo",[1561,160,1587],"func (e *Engine) WithOpenAPIInfo(info openapi.Info) *Engine Sets OpenAPI Info metadata (title, version, description, etc.).",{"id":1615,"title":1616,"titles":1617,"content":1618,"level":844},"/v0.1.21/reference/api#withtag","WithTag",[1561,160,1587],"func (e *Engine) WithTag(name, description string) *Engine Adds or updates an OpenAPI tag with description.",{"id":1620,"title":1621,"titles":1622,"content":1623,"level":844},"/v0.1.21/reference/api#withtaggroup","WithTagGroup",[1561,160,1587],"func (e *Engine) WithTagGroup(name string, tags ...string) *Engine Adds or updates a tag group for hierarchical tag organization. Rendered as the x-tagGroups vendor extension in the OpenAPI spec.",{"id":1625,"title":1626,"titles":1627,"content":1628,"level":844},"/v0.1.21/reference/api#withmodels","WithModels",[1561,160,1587],"func (e *Engine) WithModels(models ...*Model) *Engine Registers standalone types into the OpenAPI component schemas. These types don't need to be handler input or output types — they are included in the spec for use by features like discriminated unions or external references. Returns engine for chaining.",{"id":1630,"title":1631,"titles":1632,"content":1633,"level":844},"/v0.1.21/reference/api#withcodec","WithCodec",[1561,160,1587],"func (e *Engine) WithCodec(codec Codec) *Engine Sets the default codec for all handlers registered with this engine. Handlers that explicitly call WithCodec() will use their own codec instead.",{"id":1635,"title":1636,"titles":1637,"content":1638,"level":844},"/v0.1.21/reference/api#withtlsconfig","WithTLSConfig",[1561,160,1587],"func (e *Engine) WithTLSConfig(config *tls.Config) *Engine Sets the TLS configuration for the engine's HTTP server. When set, the server will use TLS (HTTPS) instead of plain HTTP. Certificates should be provided via the tls.Config (e.g., using tls.Config.Certificates or tls.Config.GetCertificate).",{"id":1640,"title":1641,"titles":1642,"content":1643,"level":844},"/v0.1.21/reference/api#router","Router",[1561,160,1587],"func (e *Engine) Router() *http.ServeMux Returns the underlying stdlib ServeMux for advanced use cases.",{"id":1645,"title":1646,"titles":1647,"content":1648,"level":844},"/v0.1.21/reference/api#generateopenapi","GenerateOpenAPI",[1561,160,1587],"func (e *Engine) GenerateOpenAPI(identity Identity) *openapi.OpenAPI Generates OpenAPI specification. Pass an Identity to filter handlers by permissions, or nil for all handlers.",{"id":1650,"title":1651,"titles":1652,"content":1653,"level":844},"/v0.1.21/reference/api#start","Start",[1561,160,1587],"func (e *Engine) Start(host string, port int) error Starts the HTTP server on the given host and port. Blocks until shutdown. ParameterTypeDescriptionhoststringHost to bind to (empty string for all interfaces)portintPort to listen on",{"id":1655,"title":1656,"titles":1657,"content":1658,"level":844},"/v0.1.21/reference/api#shutdown","Shutdown",[1561,160,1587],"func (e *Engine) Shutdown(ctx context.Context) error Gracefully shuts down the server, waiting for active requests.",{"id":1660,"title":165,"titles":1661,"content":25,"level":20},"/v0.1.21/reference/api#handler",[1561],{"id":1663,"title":1664,"titles":1665,"content":1666,"level":31},"/v0.1.21/reference/api#http-method-shortcuts","HTTP Method Shortcuts",[1561,165],"The preferred way to create handlers: func GET[In, Out any](path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out]\nfunc POST[In, Out any](path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out]\nfunc PUT[In, Out any](path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out]\nfunc PATCH[In, Out any](path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out]\nfunc DELETE[In, Out any](path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out] ParameterTypeDescriptionpathstringURL path with optional parameters (e.g., \"/users/{id}\")fnfunc(*Request[In]) (Out, error)Handler function Handler names are auto-generated from method and path with a random suffix for uniqueness (e.g., GET /users/{id} → get-users-id-a3f1b2c4).",{"id":1668,"title":1669,"titles":1670,"content":1671,"level":31},"/v0.1.21/reference/api#newhandler","NewHandler",[1561,165],"func NewHandler[In, Out any](name string, method, path string, fn func(*Request[In]) (Out, error)) *Handler[In, Out] Creates a new typed handler with explicit name. Use HTTP method shortcuts above for most cases. ParameterTypeDescriptionnamestringHandler name for logging and OpenAPI operationIdmethodstringHTTP method (GET, POST, PUT, PATCH, DELETE)pathstringURL path with optional parameters (e.g., \"/users/{id}\")fnfunc(*Request[In]) (Out, error)Handler function",{"id":1673,"title":1674,"titles":1675,"content":25,"level":31},"/v0.1.21/reference/api#handler-methods","Handler Methods",[1561,165],{"id":1677,"title":1678,"titles":1679,"content":1680,"level":844},"/v0.1.21/reference/api#withname","WithName",[1561,165,1674],"func (h *Handler[In, Out]) WithName(name string) *Handler[In, Out] Sets a custom handler name, overriding the auto-generated one. Affects OpenAPI operationId and log entries.",{"id":1682,"title":1683,"titles":1684,"content":1685,"level":844},"/v0.1.21/reference/api#withsummary","WithSummary",[1561,165,1674],"func (h *Handler[In, Out]) WithSummary(summary string) *Handler[In, Out] Sets OpenAPI summary (short description).",{"id":1687,"title":1688,"titles":1689,"content":1690,"level":844},"/v0.1.21/reference/api#withdescription","WithDescription",[1561,165,1674],"func (h *Handler[In, Out]) WithDescription(desc string) *Handler[In, Out] Sets OpenAPI description (detailed, supports markdown).",{"id":1692,"title":1693,"titles":1694,"content":1695,"level":844},"/v0.1.21/reference/api#withtags","WithTags",[1561,165,1674],"func (h *Handler[In, Out]) WithTags(tags ...string) *Handler[In, Out] Sets OpenAPI tags for grouping operations.",{"id":1697,"title":1698,"titles":1699,"content":1700,"level":844},"/v0.1.21/reference/api#withsuccessstatus","WithSuccessStatus",[1561,165,1674],"func (h *Handler[In, Out]) WithSuccessStatus(status int) *Handler[In, Out] Sets HTTP status code for successful responses. Default: 200.",{"id":1702,"title":1703,"titles":1704,"content":1705,"level":844},"/v0.1.21/reference/api#withpathparams","WithPathParams",[1561,165,1674],"func (h *Handler[In, Out]) WithPathParams(params ...string) *Handler[In, Out] Declares required path parameters.",{"id":1707,"title":1708,"titles":1709,"content":1710,"level":844},"/v0.1.21/reference/api#withqueryparams","WithQueryParams",[1561,165,1674],"func (h *Handler[In, Out]) WithQueryParams(params ...string) *Handler[In, Out] Declares query parameters.",{"id":1712,"title":1713,"titles":1714,"content":1715,"level":844},"/v0.1.21/reference/api#withresponseheaders","WithResponseHeaders",[1561,165,1674],"func (h *Handler[In, Out]) WithResponseHeaders(headers map[string]string) *Handler[In, Out] Sets default response headers.",{"id":1717,"title":1718,"titles":1719,"content":1720,"level":844},"/v0.1.21/reference/api#witherrors","WithErrors",[1561,165,1674],"func (h *Handler[In, Out]) WithErrors(errs ...ErrorDefinition) *Handler[In, Out] Declares errors this handler may return.",{"id":1722,"title":1723,"titles":1724,"content":1725,"level":844},"/v0.1.21/reference/api#withmaxbodysize","WithMaxBodySize",[1561,165,1674],"func (h *Handler[In, Out]) WithMaxBodySize(size int64) *Handler[In, Out] Sets maximum request body size in bytes. Default: 10MB.",{"id":1727,"title":1728,"titles":1729,"content":1730,"level":844},"/v0.1.21/reference/api#withoutputvalidation","WithOutputValidation",[1561,165,1674],"func (h *Handler[In, Out]) WithOutputValidation() *Handler[In, Out] Enables output validation. Disabled by default.",{"id":1732,"title":1631,"titles":1733,"content":1734,"level":844},"/v0.1.21/reference/api#withcodec-1",[1561,165,1674],"func (h *Handler[In, Out]) WithCodec(codec Codec) *Handler[In, Out] Sets the codec for request/response serialization. Overrides engine default. Default: JSON.",{"id":1736,"title":1596,"titles":1737,"content":1738,"level":844},"/v0.1.21/reference/api#withmiddleware-1",[1561,165,1674],"func (h *Handler[In, Out]) WithMiddleware(middleware ...func(http.Handler) http.Handler) *Handler[In, Out] Adds handler-specific middleware.",{"id":1740,"title":1741,"titles":1742,"content":1743,"level":844},"/v0.1.21/reference/api#withauthentication","WithAuthentication",[1561,165,1674],"func (h *Handler[In, Out]) WithAuthentication() *Handler[In, Out] Marks handler as requiring authentication.",{"id":1745,"title":1746,"titles":1747,"content":1748,"level":844},"/v0.1.21/reference/api#withscopes","WithScopes",[1561,165,1674],"func (h *Handler[In, Out]) WithScopes(scopes ...string) *Handler[In, Out] Requires one of the specified scopes (OR logic). Multiple calls create AND logic.",{"id":1750,"title":1751,"titles":1752,"content":1753,"level":844},"/v0.1.21/reference/api#withroles","WithRoles",[1561,165,1674],"func (h *Handler[In, Out]) WithRoles(roles ...string) *Handler[In, Out] Requires one of the specified roles (OR logic). Multiple calls create AND logic.",{"id":1755,"title":1756,"titles":1757,"content":1758,"level":844},"/v0.1.21/reference/api#withusagelimit","WithUsageLimit",[1561,165,1674],"func (h *Handler[In, Out]) WithUsageLimit(key string, thresholdFunc func(Identity) int) *Handler[In, Out] Adds usage limit check based on identity stats.",{"id":1760,"title":1761,"titles":1762,"content":25,"level":20},"/v0.1.21/reference/api#streamhandler","StreamHandler",[1561],{"id":1764,"title":1765,"titles":1766,"content":1767,"level":31},"/v0.1.21/reference/api#newstreamhandler","NewStreamHandler",[1561,1761],"func NewStreamHandler[In, Out any](name string, method, path string, fn func(*Request[In], Stream[Out]) error) *StreamHandler[In, Out] Creates a new typed streaming handler for Server-Sent Events. ParameterTypeDescriptionnamestringHandler name for logging and documentationmethodstringHTTP method (typically GET or POST)pathstringURL path with optional parametersfnfunc(*Request[In], Stream[Out]) errorStream handler function",{"id":1769,"title":1770,"titles":1771,"content":1772,"level":31},"/v0.1.21/reference/api#streamhandler-methods","StreamHandler Methods",[1561,1761],"StreamHandler supports the same builder methods as Handler: WithSummary(summary string) - Sets OpenAPI summaryWithDescription(desc string) - Sets OpenAPI descriptionWithTags(tags ...string) - Sets OpenAPI tagsWithPathParams(params ...string) - Declares path parametersWithQueryParams(params ...string) - Declares query parametersWithErrors(errs ...ErrorDefinition) - Declares possible errorsWithMiddleware(middleware ...func(http.Handler) http.Handler) - Adds middlewareWithAuthentication() - Requires authenticationWithScopes(scopes ...string) - Requires scopesWithRoles(roles ...string) - Requires roles",{"id":1774,"title":1775,"titles":1776,"content":1777,"level":20},"/v0.1.21/reference/api#stream","Stream",[1561],"type Stream[T any] interface {\n    Send(data T) error\n    SendEvent(event string, data T) error\n    SendComment(comment string) error\n    Done() \u003C-chan struct{}\n} Interface for sending SSE events.",{"id":1779,"title":1780,"titles":1781,"content":1782,"level":31},"/v0.1.21/reference/api#send","Send",[1561,1775],"func (s Stream[T]) Send(data T) error Sends a data-only event. Data is JSON-encoded.",{"id":1784,"title":1785,"titles":1786,"content":1787,"level":31},"/v0.1.21/reference/api#sendevent","SendEvent",[1561,1775],"func (s Stream[T]) SendEvent(event string, data T) error Sends a named event with data. Allows client-side event filtering.",{"id":1789,"title":1790,"titles":1791,"content":1792,"level":31},"/v0.1.21/reference/api#sendcomment","SendComment",[1561,1775],"func (s Stream[T]) SendComment(comment string) error Sends a comment (prefixed with :). Useful for keep-alive.",{"id":1794,"title":1795,"titles":1796,"content":1797,"level":31},"/v0.1.21/reference/api#done","Done",[1561,1775],"func (s Stream[T]) Done() \u003C-chan struct{} Returns a channel closed when the client disconnects. Use in select statements to detect disconnection.",{"id":1799,"title":180,"titles":1800,"content":1801,"level":20},"/v0.1.21/reference/api#request",[1561],"type Request[In any] struct {\n    context.Context // Embedded for deadline, cancellation, values\n    *http.Request   // Embedded for direct access when needed\n    Params          *Params\n    Body            In\n    Identity        Identity\n} FieldTypeDescriptioncontext.ContextembeddedRequest context (deadline, cancellation, values)*http.RequestembeddedUnderlying HTTP request (use sparingly)Params*ParamsPath and query parametersBodyInParsed and validated request bodyIdentityIdentityAuthenticated identity (or NoIdentity)",{"id":1803,"title":1804,"titles":1805,"content":1806,"level":20},"/v0.1.21/reference/api#params","Params",[1561],"type Params struct {\n    Path  map[string]string\n    Query map[string]string\n} FieldTypeDescriptionPathmap[string]stringPath parameters (e.g., {id})Querymap[string]stringQuery parameters",{"id":1808,"title":1809,"titles":1810,"content":1811,"level":20},"/v0.1.21/reference/api#nobody","NoBody",[1561],"type NoBody struct{} Empty struct for handlers without request bodies.",{"id":1813,"title":245,"titles":1814,"content":1815,"level":20},"/v0.1.21/reference/api#identity",[1561],"type Identity interface {\n    ID() string\n    TenantID() string\n    HasScope(scope string) bool\n    HasRole(role string) bool\n    Stats() map[string]int\n} MethodReturnDescriptionID()stringUnique identifierTenantID()stringTenant/organization IDHasScope(scope)boolCheck if identity has scopeHasRole(role)boolCheck if identity has roleStats()map[string]intUsage statistics for rate limiting",{"id":1817,"title":1818,"titles":1819,"content":1820,"level":20},"/v0.1.21/reference/api#noidentity","NoIdentity",[1561],"type NoIdentity struct{} Default identity for unauthenticated requests. All methods return empty/false values.",{"id":1822,"title":1823,"titles":1824,"content":1825,"level":20},"/v0.1.21/reference/api#engineconfig","EngineConfig",[1561],"type EngineConfig struct {\n    Host         string\n    Port         int\n    ReadTimeout  time.Duration\n    WriteTimeout time.Duration\n    IdleTimeout  time.Duration\n} FieldTypeDefaultDescriptionHoststring-Bind hostPortint-Listen portReadTimeouttime.Duration120sRead timeoutWriteTimeouttime.Duration120sWrite timeoutIdleTimeouttime.Duration120sIdle timeout",{"id":1827,"title":1828,"titles":1829,"content":1830,"level":20},"/v0.1.21/reference/api#enginespec","EngineSpec",[1561],"type EngineSpec struct {\n    Info      openapi.Info\n    Tags      []openapi.Tag\n    TagGroups []openapi.TagGroup\n} OpenAPI specification configuration.",{"id":1832,"title":1833,"titles":1834,"content":1835,"level":20},"/v0.1.21/reference/api#handlerspec","HandlerSpec",[1561],"type HandlerSpec struct {\n    Name           string\n    Method         string\n    Path           string\n    Summary        string\n    Description    string\n    Tags           []string\n    PathParams     []string\n    QueryParams    []string\n    InputTypeName  string\n    OutputTypeName string\n    SuccessStatus  int\n    ErrorCodes     []int\n    ContentType    string\n    RequiresAuth   bool\n    ScopeGroups    [][]string\n    RoleGroups     [][]string\n    UsageLimits    []UsageLimit\n} Handler metadata for OpenAPI generation.",{"id":1837,"title":1838,"titles":1839,"content":1840,"level":20},"/v0.1.21/reference/api#usagelimit","UsageLimit",[1561],"type UsageLimit struct {\n    Key           string\n    ThresholdFunc func(Identity) int\n} Usage limit configuration for rate limiting.",{"id":1842,"title":1843,"titles":1844,"content":25,"level":20},"/v0.1.21/reference/api#model","Model",[1561],{"id":1846,"title":1847,"titles":1848,"content":1849,"level":31},"/v0.1.21/reference/api#newmodel","NewModel",[1561,1843],"func NewModel[T any]() *Model Scans T with sentinel and returns a Model for OpenAPI schema registration. Use with Engine.WithModels() to include types in the spec that aren't handler input or output types. engine.WithModels(\n    rocco.NewModel[IngestCompletedEvent](),\n    rocco.NewModel[IngestFailedEvent](),\n)",{"id":1851,"title":1852,"titles":1853,"content":1854,"level":20},"/v0.1.21/reference/api#codec","Codec",[1561],"type Codec interface {\n    ContentType() string\n    Marshal(v any) ([]byte, error)\n    Unmarshal(data []byte, v any) error\n} Interface for request/response serialization. MethodReturnDescriptionContentType()stringMIME type (e.g., \"application/json\")Marshal(v)([]byte, error)Encodes value to bytesUnmarshal(data, v)errorDecodes bytes into value",{"id":1856,"title":1857,"titles":1858,"content":1859,"level":31},"/v0.1.21/reference/api#jsoncodec","JSONCodec",[1561,1852],"type JSONCodec struct{} Default codec implementation using encoding/json.",{"id":1861,"title":1862,"titles":1863,"content":1864,"level":20},"/v0.1.21/reference/api#validatable","Validatable",[1561],"type Validatable interface {\n    Validate() error\n} Interface for types that can validate themselves. Input and output types implement this interface to opt-in to automatic validation.",{"id":1866,"title":1867,"titles":1868,"content":1869,"level":31},"/v0.1.21/reference/api#newvalidationerror","NewValidationError",[1561,1862],"func NewValidationError(fields []ValidationFieldError) error Creates a validation error with field-level details. Use this in your Validate() implementations to return structured validation errors that rocco can format correctly.",{"id":1871,"title":867,"titles":1872,"content":1873,"level":31},"/v0.1.21/reference/api#example",[1561,1862],"import \"github.com/zoobz-io/check\"\n\ntype CreateUserInput struct {\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc (c CreateUserInput) Validate() error {\n    return check.All(\n        check.Required(c.Name, \"name\"),\n        check.Email(c.Email, \"email\"),\n    )\n}",{"id":1875,"title":1876,"titles":1877,"content":1878,"level":20},"/v0.1.21/reference/api#endpoint","Endpoint",[1561],"type Endpoint interface {\n    Process(ctx context.Context, r *http.Request, w http.ResponseWriter) (int, error)\n    Spec() HandlerSpec\n    ErrorDefs() []ErrorDefinition\n    Middleware() []func(http.Handler) http.Handler\n    Close() error\n} Interface implemented by Handler. MethodDescriptionProcess(...)Handles the HTTP request and writes the responseSpec()Returns the declarative specification for this handlerErrorDefs()Returns declared error definitions for OpenAPI generationMiddleware()Returns handler-specific middlewareClose()Lifecycle cleanup",{"id":1880,"title":98,"titles":1881,"content":1882,"level":20},"/v0.1.21/reference/api#see-also",[1561],"Errors Reference - Error typesEvents Reference - Event signalsCore Concepts - Usage overview html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}",{"id":1884,"title":1885,"titles":1886,"content":1887,"level":9},"/v0.1.21/reference/errors","Errors Reference",[],"Complete reference for error types and details",{"id":1889,"title":1885,"titles":1890,"content":1891,"level":9},"/v0.1.21/reference/errors#errors-reference",[],"Complete reference for rocco error types and detail structures.",{"id":1893,"title":1894,"titles":1895,"content":25,"level":20},"/v0.1.21/reference/errors#error-interface","Error Interface",[1885],{"id":1897,"title":1898,"titles":1899,"content":1900,"level":31},"/v0.1.21/reference/errors#errordefinition","ErrorDefinition",[1885,1894],"type ErrorDefinition interface {\n    error\n    Code() string\n    Status() int\n    Message() string\n    DetailsAny() any\n    DetailsMeta() sentinel.Metadata\n} MethodDescriptionError()Returns error message (implements error interface)Code()Returns error code (e.g., \"NOT_FOUND\")Status()Returns HTTP status codeMessage()Returns human-readable messageDetailsAny()Returns details as any typeDetailsMeta()Returns sentinel metadata for OpenAPI schema generation",{"id":1902,"title":1903,"titles":1904,"content":25,"level":20},"/v0.1.21/reference/errors#creating-custom-errors","Creating Custom Errors",[1885],{"id":1906,"title":1907,"titles":1908,"content":1909,"level":31},"/v0.1.21/reference/errors#newerror","NewError",[1885,1903],"func NewError[D any](code string, status int, message string) *Error[D] Creates a new error type with typed details. ParameterTypeDescriptionDtype parameterDetails typecodestringError code (e.g., \"TEAPOT\")statusintHTTP status codemessagestringDefault message",{"id":1911,"title":867,"titles":1912,"content":1913,"level":31},"/v0.1.21/reference/errors#example",[1885,1903],"type TeapotDetails struct {\n    TeaType string `json:\"tea_type\" description:\"Type of tea\"`\n}\n\nvar ErrTeapot = rocco.NewError[TeapotDetails](\"TEAPOT\", 418, \"I'm a teapot\")\n\n// Usage\nreturn Output{}, ErrTeapot.WithDetails(TeapotDetails{TeaType: \"Earl Grey\"})",{"id":1915,"title":523,"titles":1916,"content":25,"level":20},"/v0.1.21/reference/errors#built-in-errors",[1885],{"id":1918,"title":1919,"titles":1920,"content":1921,"level":31},"/v0.1.21/reference/errors#errbadrequest","ErrBadRequest",[1885,523],"var ErrBadRequest = NewError[BadRequestDetails](\"BAD_REQUEST\", 400, \"bad request\") Status: 400 Bad Request Details: type BadRequestDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Reason for bad request\"`\n}",{"id":1923,"title":1924,"titles":1925,"content":1926,"level":31},"/v0.1.21/reference/errors#errunauthorized","ErrUnauthorized",[1885,523],"var ErrUnauthorized = NewError[UnauthorizedDetails](\"UNAUTHORIZED\", 401, \"unauthorized\") Status: 401 Unauthorized Details: type UnauthorizedDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Reason for unauthorized\"`\n}",{"id":1928,"title":1929,"titles":1930,"content":1931,"level":31},"/v0.1.21/reference/errors#errforbidden","ErrForbidden",[1885,523],"var ErrForbidden = NewError[ForbiddenDetails](\"FORBIDDEN\", 403, \"forbidden\") Status: 403 Forbidden Details: type ForbiddenDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Reason for forbidden\"`\n}",{"id":1933,"title":1934,"titles":1935,"content":1936,"level":31},"/v0.1.21/reference/errors#errnotfound","ErrNotFound",[1885,523],"var ErrNotFound = NewError[NotFoundDetails](\"NOT_FOUND\", 404, \"not found\") Status: 404 Not Found Details: type NotFoundDetails struct {\n    Resource string `json:\"resource,omitempty\" description:\"Resource type that was not found\"`\n}",{"id":1938,"title":1939,"titles":1940,"content":1941,"level":31},"/v0.1.21/reference/errors#errconflict","ErrConflict",[1885,523],"var ErrConflict = NewError[ConflictDetails](\"CONFLICT\", 409, \"conflict\") Status: 409 Conflict Details: type ConflictDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"What caused the conflict\"`\n}",{"id":1943,"title":1944,"titles":1945,"content":1946,"level":31},"/v0.1.21/reference/errors#errpayloadtoolarge","ErrPayloadTooLarge",[1885,523],"var ErrPayloadTooLarge = NewError[PayloadTooLargeDetails](\"PAYLOAD_TOO_LARGE\", 413, \"payload too large\") Status: 413 Payload Too Large Details: type PayloadTooLargeDetails struct {\n    MaxSize int64 `json:\"max_size,omitempty\" description:\"Maximum allowed payload size in bytes\"`\n}",{"id":1948,"title":1949,"titles":1950,"content":1951,"level":31},"/v0.1.21/reference/errors#errunprocessableentity","ErrUnprocessableEntity",[1885,523],"var ErrUnprocessableEntity = NewError[UnprocessableEntityDetails](\"UNPROCESSABLE_ENTITY\", 422, \"unprocessable entity\") Status: 422 Unprocessable Entity Details: type UnprocessableEntityDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Reason entity is unprocessable\"`\n}",{"id":1953,"title":1954,"titles":1955,"content":1956,"level":31},"/v0.1.21/reference/errors#errvalidationfailed","ErrValidationFailed",[1885,523],"var ErrValidationFailed = NewError[ValidationDetails](\"VALIDATION_FAILED\", 422, \"validation failed\") Status: 422 Unprocessable Entity Details: type ValidationDetails struct {\n    Fields []ValidationFieldError `json:\"fields\" description:\"List of field validation errors\"`\n}\n\ntype ValidationFieldError struct {\n    Field   string `json:\"field\" description:\"Field name that failed validation\"`\n    Message string `json:\"message\" description:\"Description of the validation failure\"`\n}",{"id":1958,"title":1959,"titles":1960,"content":1961,"level":31},"/v0.1.21/reference/errors#errtoomanyrequests","ErrTooManyRequests",[1885,523],"var ErrTooManyRequests = NewError[TooManyRequestsDetails](\"TOO_MANY_REQUESTS\", 429, \"too many requests\") Status: 429 Too Many Requests Details: type TooManyRequestsDetails struct {\n    RetryAfter int `json:\"retry_after,omitempty\" description:\"Seconds until retry is allowed\"`\n}",{"id":1963,"title":1964,"titles":1965,"content":1966,"level":31},"/v0.1.21/reference/errors#errinternalserver","ErrInternalServer",[1885,523],"var ErrInternalServer = NewError[InternalServerDetails](\"INTERNAL_SERVER_ERROR\", 500, \"internal server error\") Status: 500 Internal Server Error Details: type InternalServerDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Internal error context\"`\n}",{"id":1968,"title":1969,"titles":1970,"content":1971,"level":31},"/v0.1.21/reference/errors#errnotimplemented","ErrNotImplemented",[1885,523],"var ErrNotImplemented = NewError[NotImplementedDetails](\"NOT_IMPLEMENTED\", 501, \"not implemented\") Status: 501 Not Implemented Details: type NotImplementedDetails struct {\n    Feature string `json:\"feature,omitempty\" description:\"Feature not implemented\"`\n}",{"id":1973,"title":1974,"titles":1975,"content":1976,"level":31},"/v0.1.21/reference/errors#errserviceunavailable","ErrServiceUnavailable",[1885,523],"var ErrServiceUnavailable = NewError[ServiceUnavailableDetails](\"SERVICE_UNAVAILABLE\", 503, \"service unavailable\") Status: 503 Service Unavailable Details: type ServiceUnavailableDetails struct {\n    Reason string `json:\"reason,omitempty\" description:\"Reason for unavailability\"`\n}",{"id":1978,"title":518,"titles":1979,"content":1980,"level":20},"/v0.1.21/reference/errors#error-response-format",[1885],"All errors serialize to: {\n  \"code\": \"ERROR_CODE\",\n  \"message\": \"Human-readable message\",\n  \"details\": {\n    // Optional typed details\n  }\n} FieldTypeDescriptioncodestringMachine-readable error codemessagestringHuman-readable messagedetailsobjectOptional structured details (omitted if nil)",{"id":1982,"title":1983,"titles":1984,"content":25,"level":20},"/v0.1.21/reference/errors#usage-patterns","Usage Patterns",[1885],{"id":1986,"title":1987,"titles":1988,"content":1989,"level":31},"/v0.1.21/reference/errors#simple-error","Simple Error",[1885,1983],"return Output{}, rocco.ErrNotFound Response: {\n  \"code\": \"NOT_FOUND\",\n  \"message\": \"not found\"\n}",{"id":1991,"title":537,"titles":1992,"content":1993,"level":31},"/v0.1.21/reference/errors#with-custom-message",[1885,1983],"return Output{}, rocco.ErrNotFound.WithMessage(\"user not found\") Response: {\n  \"code\": \"NOT_FOUND\",\n  \"message\": \"user not found\"\n}",{"id":1995,"title":1996,"titles":1997,"content":1998,"level":31},"/v0.1.21/reference/errors#with-details","With Details",[1885,1983],"return Output{}, rocco.ErrNotFound.WithDetails(rocco.NotFoundDetails{\n    Resource: \"user\",\n}) Response: {\n  \"code\": \"NOT_FOUND\",\n  \"message\": \"not found\",\n  \"details\": {\n    \"resource\": \"user\"\n  }\n}",{"id":2000,"title":2001,"titles":2002,"content":2003,"level":31},"/v0.1.21/reference/errors#with-cause-for-logging","With Cause (for logging)",[1885,1983],"return Output{}, rocco.ErrInternalServer.WithCause(err) The cause is logged but not exposed to clients.",{"id":2005,"title":2006,"titles":2007,"content":2008,"level":31},"/v0.1.21/reference/errors#combined","Combined",[1885,1983],"return Output{}, rocco.ErrConflict.\n    WithMessage(\"email already registered\").\n    WithDetails(rocco.ConflictDetails{\n        Reason: \"email address is already in use\",\n    })",{"id":2010,"title":98,"titles":2011,"content":2012,"level":20},"/v0.1.21/reference/errors#see-also",[1885],"Error Handling Guide - Error patternsAPI Reference - Complete API docs html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"id":2014,"title":2015,"titles":2016,"content":2017,"level":9},"/v0.1.21/reference/events","Events Reference",[],"Complete reference for event signals and field keys",{"id":2019,"title":2015,"titles":2020,"content":2021,"level":9},"/v0.1.21/reference/events#events-reference",[],"Complete reference for rocco event signals and field keys.",{"id":2023,"title":2024,"titles":2025,"content":2026,"level":20},"/v0.1.21/reference/events#event-system","Event System",[2015],"Rocco emits events via capitan. Hook into events for logging, metrics, and tracing. import \"github.com/zoobz-io/capitan\"\n\n// Hook specific event\ncapitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n    method, _ := rocco.MethodKey.From(e)\n    // ...\n})\n\n// Observe all events\ncapitan.Observe(func(ctx context.Context, e *capitan.Event) {\n    // Handle any event\n})",{"id":2028,"title":2029,"titles":2030,"content":25,"level":20},"/v0.1.21/reference/events#server-lifecycle-events","Server Lifecycle Events",[2015],{"id":2032,"title":2033,"titles":2034,"content":2035,"level":31},"/v0.1.21/reference/events#enginecreated","EngineCreated",[2015,2029],"Signal: http.engine.createdLevel: Debug Emitted when engine instance is created. No fields.",{"id":2037,"title":2038,"titles":2039,"content":2040,"level":31},"/v0.1.21/reference/events#enginestarting","EngineStarting",[2015,2029],"Signal: http.engine.startingLevel: Info Emitted when server starts listening. FieldTypeDescriptionAddressKeystringAddress being listened onTLSEnabledKeyboolWhether TLS is enabled",{"id":2042,"title":2043,"titles":2044,"content":2045,"level":31},"/v0.1.21/reference/events#engineshutdownstarted","EngineShutdownStarted",[2015,2029],"Signal: http.engine.shutdown.startedLevel: Info Emitted when shutdown is initiated. No fields.",{"id":2047,"title":2048,"titles":2049,"content":2050,"level":31},"/v0.1.21/reference/events#engineshutdowncomplete","EngineShutdownComplete",[2015,2029],"Signal: http.engine.shutdown.completeLevel: Info/Error Emitted when shutdown completes. FieldTypeDescriptionGracefulKeyboolWhether shutdown was gracefulErrorKeystringError message (if failed)",{"id":2052,"title":2053,"titles":2054,"content":25,"level":20},"/v0.1.21/reference/events#handler-registration-events","Handler Registration Events",[2015],{"id":2056,"title":2057,"titles":2058,"content":2059,"level":31},"/v0.1.21/reference/events#handlerregistered","HandlerRegistered",[2015,2053],"Signal: http.handler.registeredLevel: Debug Emitted when a handler is registered. FieldTypeDescriptionHandlerNameKeystringHandler nameMethodKeystringHTTP methodPathKeystringURL path",{"id":2061,"title":2062,"titles":2063,"content":25,"level":20},"/v0.1.21/reference/events#request-lifecycle-events","Request Lifecycle Events",[2015],{"id":2065,"title":2066,"titles":2067,"content":2068,"level":31},"/v0.1.21/reference/events#requestreceived","RequestReceived",[2015,2062],"Signal: http.request.receivedLevel: Debug Emitted when request is received. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler name",{"id":2070,"title":2071,"titles":2072,"content":2073,"level":31},"/v0.1.21/reference/events#requestcompleted","RequestCompleted",[2015,2062],"Signal: http.request.completedLevel: Info Emitted when request completes successfully. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameStatusCodeKeyintHTTP status codeDurationMsKeyint64Duration in milliseconds",{"id":2075,"title":2076,"titles":2077,"content":2078,"level":31},"/v0.1.21/reference/events#requestfailed","RequestFailed",[2015,2062],"Signal: http.request.failedLevel: Error Emitted when request fails with error. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameStatusCodeKeyintHTTP status codeDurationMsKeyint64Duration in millisecondsErrorKeystringError message",{"id":2080,"title":2081,"titles":2082,"content":25,"level":20},"/v0.1.21/reference/events#handler-execution-events","Handler Execution Events",[2015],{"id":2084,"title":2085,"titles":2086,"content":2087,"level":31},"/v0.1.21/reference/events#handlerexecuting","HandlerExecuting",[2015,2081],"Signal: http.handler.executingLevel: Debug Emitted when handler function starts. FieldTypeDescriptionHandlerNameKeystringHandler name",{"id":2089,"title":2090,"titles":2091,"content":2092,"level":31},"/v0.1.21/reference/events#handlersuccess","HandlerSuccess",[2015,2081],"Signal: http.handler.successLevel: Info Emitted when handler returns successfully. FieldTypeDescriptionHandlerNameKeystringHandler nameStatusCodeKeyintSuccess status code",{"id":2094,"title":2095,"titles":2096,"content":2097,"level":31},"/v0.1.21/reference/events#handlererror","HandlerError",[2015,2081],"Signal: http.handler.errorLevel: Error Emitted when handler returns unexpected error. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2099,"title":2100,"titles":2101,"content":2102,"level":31},"/v0.1.21/reference/events#handlersentinelerror","HandlerSentinelError",[2015,2081],"Signal: http.handler.sentinel.errorLevel: Warn Emitted when handler returns declared sentinel error. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError messageStatusCodeKeyintError status code",{"id":2104,"title":2105,"titles":2106,"content":2107,"level":31},"/v0.1.21/reference/events#handlerundeclaredsentinel","HandlerUndeclaredSentinel",[2015,2081],"Signal: http.handler.sentinel.undeclaredLevel: Warn Emitted when handler returns undeclared sentinel error (programming error). FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError messageStatusCodeKeyintWould-be status code",{"id":2109,"title":2110,"titles":2111,"content":25,"level":20},"/v0.1.21/reference/events#request-processing-events","Request Processing Events",[2015],{"id":2113,"title":2114,"titles":2115,"content":2116,"level":31},"/v0.1.21/reference/events#requestparamsinvalid","RequestParamsInvalid",[2015,2110],"Signal: http.request.params.invalidLevel: Error Emitted when path/query parameter extraction fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2118,"title":2119,"titles":2120,"content":2121,"level":31},"/v0.1.21/reference/events#requestbodyreaderror","RequestBodyReadError",[2015,2110],"Signal: http.request.body.read.errorLevel: Error/Warn Emitted when request body read fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2123,"title":2124,"titles":2125,"content":2126,"level":31},"/v0.1.21/reference/events#requestbodyparseerror","RequestBodyParseError",[2015,2110],"Signal: http.request.body.parse.errorLevel: Error Emitted when JSON body parsing fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2128,"title":2129,"titles":2130,"content":2131,"level":31},"/v0.1.21/reference/events#requestvalidationinputfailed","RequestValidationInputFailed",[2015,2110],"Signal: http.request.validation.input.failedLevel: Warn Emitted when input validation fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringValidation errors",{"id":2133,"title":2134,"titles":2135,"content":2136,"level":31},"/v0.1.21/reference/events#requestvalidationoutputfailed","RequestValidationOutputFailed",[2015,2110],"Signal: http.request.validation.output.failedLevel: Warn Emitted when output validation fails (if enabled). FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringValidation errors",{"id":2138,"title":2139,"titles":2140,"content":2141,"level":31},"/v0.1.21/reference/events#requestresponsemarshalerror","RequestResponseMarshalError",[2015,2110],"Signal: http.request.response.marshal.errorLevel: Error Emitted when response JSON marshaling fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2143,"title":2144,"titles":2145,"content":2146,"level":31},"/v0.1.21/reference/events#requestbodycloseerror","RequestBodyCloseError",[2015,2110],"Signal: http.request.body.close.errorLevel: Warn Emitted when closing the request body stream fails. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2148,"title":2149,"titles":2150,"content":2151,"level":31},"/v0.1.21/reference/events#responsewriteerror","ResponseWriteError",[2015,2110],"Signal: http.response.write.errorLevel: Warn Emitted when writing response body to client fails (e.g., client disconnected). FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2153,"title":2154,"titles":2155,"content":25,"level":20},"/v0.1.21/reference/events#authentication-events","Authentication Events",[2015],{"id":2157,"title":2158,"titles":2159,"content":2160,"level":31},"/v0.1.21/reference/events#authenticationfailed","AuthenticationFailed",[2015,2154],"Signal: http.auth.failedLevel: Warn Emitted when identity extraction fails. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2162,"title":2163,"titles":2164,"content":2165,"level":31},"/v0.1.21/reference/events#authenticationsucceeded","AuthenticationSucceeded",[2015,2154],"Signal: http.auth.succeededLevel: Debug Emitted when identity extraction succeeds. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameIdentityIDKeystringIdentity IDTenantIDKeystringTenant ID",{"id":2167,"title":2168,"titles":2169,"content":25,"level":20},"/v0.1.21/reference/events#authorization-events","Authorization Events",[2015],{"id":2171,"title":2172,"titles":2173,"content":2174,"level":31},"/v0.1.21/reference/events#authorizationscopedenied","AuthorizationScopeDenied",[2015,2168],"Signal: http.authz.scope.deniedLevel: Warn Emitted when scope check fails. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameIdentityIDKeystringIdentity IDRequiredScopesKeystringRequired scopes (comma-separated)UserScopesKeystringUser's scopes (comma-separated)",{"id":2176,"title":2177,"titles":2178,"content":2179,"level":31},"/v0.1.21/reference/events#authorizationroledenied","AuthorizationRoleDenied",[2015,2168],"Signal: http.authz.role.deniedLevel: Warn Emitted when role check fails. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameIdentityIDKeystringIdentity IDRequiredRolesKeystringRequired roles (comma-separated)UserRolesKeystringUser's roles (comma-separated)",{"id":2181,"title":2182,"titles":2183,"content":2184,"level":31},"/v0.1.21/reference/events#authorizationsucceeded","AuthorizationSucceeded",[2015,2168],"Signal: http.authz.succeededLevel: Debug Emitted when authorization passes. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameIdentityIDKeystringIdentity ID",{"id":2186,"title":2187,"titles":2188,"content":25,"level":20},"/v0.1.21/reference/events#rate-limiting-events","Rate Limiting Events",[2015],{"id":2190,"title":2191,"titles":2192,"content":2193,"level":31},"/v0.1.21/reference/events#ratelimitexceeded","RateLimitExceeded",[2015,2187],"Signal: http.ratelimit.exceededLevel: Warn Emitted when usage limit is exceeded. FieldTypeDescriptionMethodKeystringHTTP methodPathKeystringRequest pathIdentityIDKeystringIdentity IDLimitKeyKeystringLimit key nameCurrentValueKeyintCurrent usage valueThresholdKeyintLimit threshold",{"id":2195,"title":2196,"titles":2197,"content":25,"level":20},"/v0.1.21/reference/events#stream-sse-events","Stream (SSE) Events",[2015],{"id":2199,"title":2200,"titles":2201,"content":2202,"level":31},"/v0.1.21/reference/events#streamexecuting","StreamExecuting",[2015,2196],"Signal: http.stream.executingLevel: Debug Emitted when stream handler starts. FieldTypeDescriptionHandlerNameKeystringHandler name",{"id":2204,"title":2205,"titles":2206,"content":2207,"level":31},"/v0.1.21/reference/events#streamstarted","StreamStarted",[2015,2196],"Signal: http.stream.startedLevel: Info Emitted when SSE stream is established and headers sent. FieldTypeDescriptionHandlerNameKeystringHandler name",{"id":2209,"title":2210,"titles":2211,"content":2212,"level":31},"/v0.1.21/reference/events#streamended","StreamEnded",[2015,2196],"Signal: http.stream.endedLevel: Info Emitted when stream handler completes normally. FieldTypeDescriptionHandlerNameKeystringHandler name",{"id":2214,"title":2215,"titles":2216,"content":2217,"level":31},"/v0.1.21/reference/events#streamclientdisconnected","StreamClientDisconnected",[2015,2196],"Signal: http.stream.client.disconnectedLevel: Info Emitted when client disconnects from stream. FieldTypeDescriptionHandlerNameKeystringHandler name",{"id":2219,"title":2220,"titles":2221,"content":2222,"level":31},"/v0.1.21/reference/events#streamerror","StreamError",[2015,2196],"Signal: http.stream.errorLevel: Error Emitted when stream handler encounters error. FieldTypeDescriptionHandlerNameKeystringHandler nameErrorKeystringError message",{"id":2224,"title":2225,"titles":2226,"content":2227,"level":20},"/v0.1.21/reference/events#field-keys-reference","Field Keys Reference",[2015],"KeyTypeDescriptionAddressKeystringServer address (e.g., \":8080\")MethodKeystringHTTP methodPathKeystringRequest pathHandlerNameKeystringHandler nameStatusCodeKeyintHTTP status codeDurationMsKeyint64Duration in millisecondsErrorKeystringError messageGracefulKeyboolGraceful shutdown flagIdentityIDKeystringIdentity IDTenantIDKeystringTenant IDRequiredScopesKeystringRequired scopesUserScopesKeystringUser's scopesRequiredRolesKeystringRequired rolesUserRolesKeystringUser's rolesLimitKeyKeystringUsage limit keyCurrentValueKeyintCurrent usage valueThresholdKeyintUsage threshold",{"id":2229,"title":2230,"titles":2231,"content":2232,"level":20},"/v0.1.21/reference/events#usage-example","Usage Example",[2015],"import (\n    \"context\"\n    \"github.com/zoobz-io/capitan\"\n    \"github.com/zoobz-io/rocco\"\n)\n\nfunc setupObservability() {\n    // Log all requests\n    capitan.Hook(rocco.RequestCompleted, func(ctx context.Context, e *capitan.Event) {\n        method, _ := rocco.MethodKey.From(e)\n        path, _ := rocco.PathKey.From(e)\n        status, _ := rocco.StatusCodeKey.From(e)\n        duration, _ := rocco.DurationMsKey.From(e)\n\n        log.Info(\"request\",\n            \"method\", method,\n            \"path\", path,\n            \"status\", status,\n            \"duration_ms\", duration,\n        )\n    })\n\n    // Alert on auth failures\n    capitan.Hook(rocco.AuthenticationFailed, func(ctx context.Context, e *capitan.Event) {\n        path, _ := rocco.PathKey.From(e)\n        err, _ := rocco.ErrorKey.From(e)\n        alerting.Warn(\"auth_failed\", path, err)\n    })\n}",{"id":2234,"title":98,"titles":2235,"content":2236,"level":20},"/v0.1.21/reference/events#see-also",[2015],"Observability Cookbook - Implementation patternscapitan Documentation - Event system docs html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",[2238],{"title":2239,"path":2240,"stem":2241,"children":2242,"page":2256},"V0121","/v0.1.21","v0.1.21",[2243,2245,2257,2274,2287],{"title":6,"path":5,"stem":2244,"description":8},"v0.1.21/1.overview",{"title":2246,"path":2247,"stem":2248,"children":2249,"page":2256},"Learn","/v0.1.21/learn","v0.1.21/2.learn",[2250,2252,2254],{"title":103,"path":102,"stem":2251,"description":105},"v0.1.21/2.learn/1.quickstart",{"title":151,"path":150,"stem":2253,"description":153},"v0.1.21/2.learn/2.concepts",{"title":258,"path":257,"stem":2255,"description":260},"v0.1.21/2.learn/3.architecture",false,{"title":2258,"path":2259,"stem":2260,"children":2261,"page":2256},"Guides","/v0.1.21/guides","v0.1.21/3.guides",[2262,2264,2266,2268,2270,2272],{"title":346,"path":345,"stem":2263,"description":348},"v0.1.21/3.guides/1.handlers",{"title":457,"path":508,"stem":2265,"description":510},"v0.1.21/3.guides/2.errors",{"title":652,"path":651,"stem":2267,"description":654},"v0.1.21/3.guides/3.authentication",{"title":63,"path":782,"stem":2269,"description":784},"v0.1.21/3.guides/4.openapi",{"title":619,"path":953,"stem":2271,"description":955},"v0.1.21/3.guides/5.best-practices",{"title":1109,"path":1108,"stem":2273,"description":1111},"v0.1.21/3.guides/6.streaming",{"title":2275,"path":2276,"stem":2277,"children":2278,"page":2256},"Cookbook","/v0.1.21/cookbook","v0.1.21/4.cookbook",[2279,2281,2283,2285],{"title":1209,"path":1208,"stem":2280,"description":1211},"v0.1.21/4.cookbook/1.crud-api",{"title":1284,"path":1283,"stem":2282,"description":1286},"v0.1.21/4.cookbook/2.authentication",{"title":78,"path":1437,"stem":2284,"description":1439},"v0.1.21/4.cookbook/3.observability",{"title":1508,"path":1507,"stem":2286,"description":1510},"v0.1.21/4.cookbook/4.realtime",{"title":2288,"path":2289,"stem":2290,"children":2291,"page":2256},"Reference","/v0.1.21/reference","v0.1.21/5.reference",[2292,2294,2296],{"title":1561,"path":1560,"stem":2293,"description":1563},"v0.1.21/5.reference/1.api",{"title":1885,"path":1884,"stem":2295,"description":1887},"v0.1.21/5.reference/2.errors",{"title":2015,"path":2014,"stem":2297,"description":2017},"v0.1.21/5.reference/3.events",[2299],{"title":2239,"path":2240,"stem":2241,"children":2300,"page":2256},[2301,2302,2307,2315,2321],{"title":6,"path":5,"stem":2244},{"title":2246,"path":2247,"stem":2248,"children":2303,"page":2256},[2304,2305,2306],{"title":103,"path":102,"stem":2251},{"title":151,"path":150,"stem":2253},{"title":258,"path":257,"stem":2255},{"title":2258,"path":2259,"stem":2260,"children":2308,"page":2256},[2309,2310,2311,2312,2313,2314],{"title":346,"path":345,"stem":2263},{"title":457,"path":508,"stem":2265},{"title":652,"path":651,"stem":2267},{"title":63,"path":782,"stem":2269},{"title":619,"path":953,"stem":2271},{"title":1109,"path":1108,"stem":2273},{"title":2275,"path":2276,"stem":2277,"children":2316,"page":2256},[2317,2318,2319,2320],{"title":1209,"path":1208,"stem":2280},{"title":1284,"path":1283,"stem":2282},{"title":78,"path":1437,"stem":2284},{"title":1508,"path":1507,"stem":2286},{"title":2288,"path":2289,"stem":2290,"children":2322,"page":2256},[2323,2324,2325],{"title":1561,"path":1560,"stem":2293},{"title":1885,"path":1884,"stem":2295},{"title":2015,"path":2014,"stem":2297},[2327,4019,5318],{"id":2328,"title":2329,"body":2330,"description":25,"extension":4012,"icon":4013,"meta":4014,"navigation":2478,"path":4015,"seo":4016,"stem":4017,"__hash__":4018},"resources/readme.md","README",{"type":2331,"value":2332,"toc":3996},"minimark",[2333,2337,2405,2408,2411,2416,2744,2747,2751,2768,2771,2775,3306,3310,3415,3419,3466,3470,3476,3479,3484,3539,3544,3726,3731,3844,3847,3850,3858,3862,3881,3884,3918,3921,3948,3951,3971,3975,3983,3986,3992],[2334,2335,2336],"h1",{"id":2336},"rocco",[2338,2339,2340,2351,2359,2367,2375,2383,2390,2397],"p",{},[2341,2342,2346],"a",{"href":2343,"rel":2344},"https://github.com/zoobz-io/rocco/actions/workflows/ci.yml",[2345],"nofollow",[2347,2348],"img",{"alt":2349,"src":2350},"CI Status","https://github.com/zoobz-io/rocco/workflows/CI/badge.svg",[2341,2352,2355],{"href":2353,"rel":2354},"https://codecov.io/gh/zoobz-io/rocco",[2345],[2347,2356],{"alt":2357,"src":2358},"codecov","https://codecov.io/gh/zoobz-io/rocco/graph/badge.svg?branch=main",[2341,2360,2363],{"href":2361,"rel":2362},"https://goreportcard.com/report/github.com/zoobz-io/rocco",[2345],[2347,2364],{"alt":2365,"src":2366},"Go Report Card","https://goreportcard.com/badge/github.com/zoobz-io/rocco",[2341,2368,2371],{"href":2369,"rel":2370},"https://github.com/zoobz-io/rocco/security/code-scanning",[2345],[2347,2372],{"alt":2373,"src":2374},"CodeQL","https://github.com/zoobz-io/rocco/workflows/CodeQL/badge.svg",[2341,2376,2379],{"href":2377,"rel":2378},"https://pkg.go.dev/github.com/zoobz-io/rocco",[2345],[2347,2380],{"alt":2381,"src":2382},"Go Reference","https://pkg.go.dev/badge/github.com/zoobz-io/rocco.svg",[2341,2384,2386],{"href":2385},"LICENSE",[2347,2387],{"alt":2388,"src":2389},"License","https://img.shields.io/github/license/zoobz-io/rocco",[2341,2391,2393],{"href":2392},"go.mod",[2347,2394],{"alt":2395,"src":2396},"Go Version","https://img.shields.io/github/go-mod/go-version/zoobz-io/rocco",[2341,2398,2401],{"href":2399,"rel":2400},"https://github.com/zoobz-io/rocco/releases",[2345],[2347,2402],{"alt":2403,"src":2404},"Release","https://img.shields.io/github/v/release/rocco",[2338,2406,2407],{},"Type-safe HTTP framework for Go with automatic OpenAPI generation.",[2338,2409,2410],{},"Define your request and response types, wire up handlers, and get a fully-documented API with validation baked in.",[2412,2413,2415],"h2",{"id":2414},"types-become-endpoints","Types Become Endpoints",[2417,2418,2422],"pre",{"className":2419,"code":2420,"language":2421,"meta":25,"style":25},"language-go shiki shiki-themes","type CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n\ntype UserOutput struct {\n    ID    string `json:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nhandler := rocco.NewHandler[CreateUserInput, UserOutput](\n    \"create-user\", \"POST\", \"/users\",\n    func(req *rocco.Request[CreateUserInput]) (UserOutput, error) {\n        return UserOutput{\n            ID:    \"usr_123\",\n            Name:  req.Body.Name,\n            Email: req.Body.Email,\n        }, nil\n    },\n).WithErrors(rocco.ErrBadRequest, rocco.ErrConflict)\n","go",[2423,2424,2425,2444,2457,2468,2473,2480,2492,2504,2514,2524,2529,2534,2567,2586,2632,2643,2657,2680,2702,2711,2717],"code",{"__ignoreMap":25},[2426,2427,2429,2433,2437,2440],"span",{"class":2428,"line":9},"line",[2426,2430,2432],{"class":2431},"sUt3r","type",[2426,2434,2436],{"class":2435},"sYBwO"," CreateUserInput",[2426,2438,2439],{"class":2431}," struct",[2426,2441,2443],{"class":2442},"sq5bi"," {\n",[2426,2445,2446,2450,2453],{"class":2428,"line":20},[2426,2447,2449],{"class":2448},"sBGCq","    Name",[2426,2451,2452],{"class":2435},"  string",[2426,2454,2456],{"class":2455},"sxAnc"," `json:\"name\" validate:\"required,min=2\"`\n",[2426,2458,2459,2462,2465],{"class":2428,"line":31},[2426,2460,2461],{"class":2448},"    Email",[2426,2463,2464],{"class":2435}," string",[2426,2466,2467],{"class":2455}," `json:\"email\" validate:\"required,email\"`\n",[2426,2469,2470],{"class":2428,"line":844},[2426,2471,2472],{"class":2442},"}\n",[2426,2474,2476],{"class":2428,"line":2475},5,[2426,2477,2479],{"emptyLinePlaceholder":2478},true,"\n",[2426,2481,2483,2485,2488,2490],{"class":2428,"line":2482},6,[2426,2484,2432],{"class":2431},[2426,2486,2487],{"class":2435}," UserOutput",[2426,2489,2439],{"class":2431},[2426,2491,2443],{"class":2442},[2426,2493,2495,2498,2501],{"class":2428,"line":2494},7,[2426,2496,2497],{"class":2448},"    ID",[2426,2499,2500],{"class":2435},"    string",[2426,2502,2503],{"class":2455}," `json:\"id\"`\n",[2426,2505,2507,2509,2511],{"class":2428,"line":2506},8,[2426,2508,2449],{"class":2448},[2426,2510,2452],{"class":2435},[2426,2512,2513],{"class":2455}," `json:\"name\"`\n",[2426,2515,2517,2519,2521],{"class":2428,"line":2516},9,[2426,2518,2461],{"class":2448},[2426,2520,2464],{"class":2435},[2426,2522,2523],{"class":2455}," `json:\"email\"`\n",[2426,2525,2527],{"class":2428,"line":2526},10,[2426,2528,2472],{"class":2442},[2426,2530,2532],{"class":2428,"line":2531},11,[2426,2533,2479],{"emptyLinePlaceholder":2478},[2426,2535,2537,2541,2544,2547,2550,2553,2556,2559,2562,2564],{"class":2428,"line":2536},12,[2426,2538,2540],{"class":2539},"sh8_p","handler",[2426,2542,2543],{"class":2539}," :=",[2426,2545,2546],{"class":2539}," rocco",[2426,2548,2549],{"class":2442},".",[2426,2551,1669],{"class":2552},"s5klm",[2426,2554,2555],{"class":2442},"[",[2426,2557,2558],{"class":2435},"CreateUserInput",[2426,2560,2561],{"class":2442},",",[2426,2563,2487],{"class":2435},[2426,2565,2566],{"class":2442},"](\n",[2426,2568,2570,2573,2575,2578,2580,2583],{"class":2428,"line":2569},13,[2426,2571,2572],{"class":2455},"    \"create-user\"",[2426,2574,2561],{"class":2442},[2426,2576,2577],{"class":2455}," \"POST\"",[2426,2579,2561],{"class":2442},[2426,2581,2582],{"class":2455}," \"/users\"",[2426,2584,2585],{"class":2442},",\n",[2426,2587,2589,2592,2595,2599,2603,2605,2607,2609,2611,2613,2616,2619,2622,2624,2627,2630],{"class":2428,"line":2588},14,[2426,2590,2591],{"class":2431},"    func",[2426,2593,2594],{"class":2442},"(",[2426,2596,2598],{"class":2597},"sSYET","req",[2426,2600,2602],{"class":2601},"sW3Qg"," *",[2426,2604,2336],{"class":2435},[2426,2606,2549],{"class":2442},[2426,2608,180],{"class":2435},[2426,2610,2555],{"class":2442},[2426,2612,2558],{"class":2435},[2426,2614,2615],{"class":2442},"])",[2426,2617,2618],{"class":2442}," (",[2426,2620,2621],{"class":2435},"UserOutput",[2426,2623,2561],{"class":2442},[2426,2625,2626],{"class":2435}," error",[2426,2628,2629],{"class":2442},")",[2426,2631,2443],{"class":2442},[2426,2633,2635,2638,2640],{"class":2428,"line":2634},15,[2426,2636,2637],{"class":2601},"        return",[2426,2639,2487],{"class":2435},[2426,2641,2642],{"class":2442},"{\n",[2426,2644,2646,2649,2652,2655],{"class":2428,"line":2645},16,[2426,2647,2648],{"class":2448},"            ID",[2426,2650,2651],{"class":2442},":",[2426,2653,2654],{"class":2455},"    \"usr_123\"",[2426,2656,2585],{"class":2442},[2426,2658,2660,2663,2665,2668,2670,2673,2675,2678],{"class":2428,"line":2659},17,[2426,2661,2662],{"class":2448},"            Name",[2426,2664,2651],{"class":2442},[2426,2666,2667],{"class":2539},"  req",[2426,2669,2549],{"class":2442},[2426,2671,2672],{"class":2539},"Body",[2426,2674,2549],{"class":2442},[2426,2676,2677],{"class":2539},"Name",[2426,2679,2585],{"class":2442},[2426,2681,2683,2686,2688,2691,2693,2695,2697,2700],{"class":2428,"line":2682},18,[2426,2684,2685],{"class":2448},"            Email",[2426,2687,2651],{"class":2442},[2426,2689,2690],{"class":2539}," req",[2426,2692,2549],{"class":2442},[2426,2694,2672],{"class":2539},[2426,2696,2549],{"class":2442},[2426,2698,2699],{"class":2539},"Email",[2426,2701,2585],{"class":2442},[2426,2703,2705,2708],{"class":2428,"line":2704},19,[2426,2706,2707],{"class":2442},"        },",[2426,2709,2710],{"class":2431}," nil\n",[2426,2712,2714],{"class":2428,"line":2713},20,[2426,2715,2716],{"class":2442},"    },\n",[2426,2718,2720,2723,2725,2727,2729,2731,2733,2735,2737,2739,2741],{"class":2428,"line":2719},21,[2426,2721,2722],{"class":2442},").",[2426,2724,1718],{"class":2552},[2426,2726,2594],{"class":2442},[2426,2728,2336],{"class":2539},[2426,2730,2549],{"class":2442},[2426,2732,1919],{"class":2539},[2426,2734,2561],{"class":2442},[2426,2736,2546],{"class":2539},[2426,2738,2549],{"class":2442},[2426,2740,1939],{"class":2539},[2426,2742,2743],{"class":2442},")\n",[2338,2745,2746],{},"Your types define the contract. Rocco handles validation, serialization, error responses, and OpenAPI schema generation — all derived from the same source of truth.",[2412,2748,2750],{"id":2749},"install","Install",[2417,2752,2756],{"className":2753,"code":2754,"language":2755,"meta":25,"style":25},"language-bash shiki shiki-themes","go get github.com/zoobz-io/rocco\n","bash",[2423,2757,2758],{"__ignoreMap":25},[2426,2759,2760,2762,2765],{"class":2428,"line":9},[2426,2761,2421],{"class":2552},[2426,2763,2764],{"class":2455}," get",[2426,2766,2767],{"class":2455}," github.com/zoobz-io/rocco\n",[2338,2769,2770],{},"Requires Go 1.24+.",[2412,2772,2774],{"id":2773},"quick-start","Quick Start",[2417,2776,2778],{"className":2419,"code":2777,"language":2421,"meta":25,"style":25},"package main\n\nimport (\n    \"fmt\"\n\n    \"github.com/zoobz-io/openapi\"\n    \"github.com/zoobz-io/rocco\"\n)\n\ntype CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n\ntype UserOutput struct {\n    ID    string `json:\"id\"`\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc main() {\n    engine := rocco.NewEngine()\n\n    // Configure OpenAPI metadata\n    engine.WithOpenAPIInfo(openapi.Info{\n        Title:   \"User API\",\n        Version: \"1.0.0\",\n    })\n\n    handler := rocco.NewHandler[CreateUserInput, UserOutput](\n        \"create-user\", \"POST\", \"/users\",\n        func(req *rocco.Request[CreateUserInput]) (UserOutput, error) {\n            return UserOutput{\n                ID:    \"usr_123\",\n                Name:  req.Body.Name,\n                Email: req.Body.Email,\n            }, nil\n        },\n    ).\n        WithSummary(\"Create a new user\").\n        WithTags(\"users\").\n        WithSuccessStatus(201).\n        WithErrors(rocco.ErrBadRequest, rocco.ErrUnprocessableEntity)\n\n    engine.WithHandlers(handler)\n\n    // OpenAPI spec at /openapi, interactive docs at /docs\n    fmt.Println(\"Server listening on :8080\")\n    engine.Start(rocco.HostAll, 8080)\n}\n",[2423,2779,2780,2788,2792,2801,2806,2810,2815,2820,2824,2828,2838,2846,2854,2858,2862,2872,2880,2888,2896,2900,2904,2917,2934,2939,2946,2967,2980,2993,2999,3004,3028,3044,3080,3090,3102,3122,3142,3150,3156,3162,3176,3189,3203,3227,3232,3247,3252,3258,3276,3301],{"__ignoreMap":25},[2426,2781,2782,2785],{"class":2428,"line":9},[2426,2783,2784],{"class":2431},"package",[2426,2786,2787],{"class":2435}," main\n",[2426,2789,2790],{"class":2428,"line":20},[2426,2791,2479],{"emptyLinePlaceholder":2478},[2426,2793,2794,2797],{"class":2428,"line":31},[2426,2795,2796],{"class":2431},"import",[2426,2798,2800],{"class":2799},"soy-K"," (\n",[2426,2802,2803],{"class":2428,"line":844},[2426,2804,2805],{"class":2455},"    \"fmt\"\n",[2426,2807,2808],{"class":2428,"line":2475},[2426,2809,2479],{"emptyLinePlaceholder":2478},[2426,2811,2812],{"class":2428,"line":2482},[2426,2813,2814],{"class":2455},"    \"github.com/zoobz-io/openapi\"\n",[2426,2816,2817],{"class":2428,"line":2494},[2426,2818,2819],{"class":2455},"    \"github.com/zoobz-io/rocco\"\n",[2426,2821,2822],{"class":2428,"line":2506},[2426,2823,2743],{"class":2799},[2426,2825,2826],{"class":2428,"line":2516},[2426,2827,2479],{"emptyLinePlaceholder":2478},[2426,2829,2830,2832,2834,2836],{"class":2428,"line":2526},[2426,2831,2432],{"class":2431},[2426,2833,2436],{"class":2435},[2426,2835,2439],{"class":2431},[2426,2837,2443],{"class":2442},[2426,2839,2840,2842,2844],{"class":2428,"line":2531},[2426,2841,2449],{"class":2448},[2426,2843,2452],{"class":2435},[2426,2845,2456],{"class":2455},[2426,2847,2848,2850,2852],{"class":2428,"line":2536},[2426,2849,2461],{"class":2448},[2426,2851,2464],{"class":2435},[2426,2853,2467],{"class":2455},[2426,2855,2856],{"class":2428,"line":2569},[2426,2857,2472],{"class":2442},[2426,2859,2860],{"class":2428,"line":2588},[2426,2861,2479],{"emptyLinePlaceholder":2478},[2426,2863,2864,2866,2868,2870],{"class":2428,"line":2634},[2426,2865,2432],{"class":2431},[2426,2867,2487],{"class":2435},[2426,2869,2439],{"class":2431},[2426,2871,2443],{"class":2442},[2426,2873,2874,2876,2878],{"class":2428,"line":2645},[2426,2875,2497],{"class":2448},[2426,2877,2500],{"class":2435},[2426,2879,2503],{"class":2455},[2426,2881,2882,2884,2886],{"class":2428,"line":2659},[2426,2883,2449],{"class":2448},[2426,2885,2452],{"class":2435},[2426,2887,2513],{"class":2455},[2426,2889,2890,2892,2894],{"class":2428,"line":2682},[2426,2891,2461],{"class":2448},[2426,2893,2464],{"class":2435},[2426,2895,2523],{"class":2455},[2426,2897,2898],{"class":2428,"line":2704},[2426,2899,2472],{"class":2442},[2426,2901,2902],{"class":2428,"line":2713},[2426,2903,2479],{"emptyLinePlaceholder":2478},[2426,2905,2906,2909,2912,2915],{"class":2428,"line":2719},[2426,2907,2908],{"class":2431},"func",[2426,2910,2911],{"class":2552}," main",[2426,2913,2914],{"class":2442},"()",[2426,2916,2443],{"class":2442},[2426,2918,2920,2923,2925,2927,2929,2931],{"class":2428,"line":2919},22,[2426,2921,2922],{"class":2539},"    engine",[2426,2924,2543],{"class":2539},[2426,2926,2546],{"class":2539},[2426,2928,2549],{"class":2442},[2426,2930,1582],{"class":2552},[2426,2932,2933],{"class":2442},"()\n",[2426,2935,2937],{"class":2428,"line":2936},23,[2426,2938,2479],{"emptyLinePlaceholder":2478},[2426,2940,2942],{"class":2428,"line":2941},24,[2426,2943,2945],{"class":2944},"sLkEo","    // Configure OpenAPI metadata\n",[2426,2947,2949,2951,2953,2955,2957,2960,2962,2965],{"class":2428,"line":2948},25,[2426,2950,2922],{"class":2539},[2426,2952,2549],{"class":2442},[2426,2954,1611],{"class":2552},[2426,2956,2594],{"class":2442},[2426,2958,2959],{"class":2435},"openapi",[2426,2961,2549],{"class":2442},[2426,2963,2964],{"class":2435},"Info",[2426,2966,2642],{"class":2442},[2426,2968,2970,2973,2975,2978],{"class":2428,"line":2969},26,[2426,2971,2972],{"class":2448},"        Title",[2426,2974,2651],{"class":2442},[2426,2976,2977],{"class":2455},"   \"User API\"",[2426,2979,2585],{"class":2442},[2426,2981,2983,2986,2988,2991],{"class":2428,"line":2982},27,[2426,2984,2985],{"class":2448},"        Version",[2426,2987,2651],{"class":2442},[2426,2989,2990],{"class":2455}," \"1.0.0\"",[2426,2992,2585],{"class":2442},[2426,2994,2996],{"class":2428,"line":2995},28,[2426,2997,2998],{"class":2442},"    })\n",[2426,3000,3002],{"class":2428,"line":3001},29,[2426,3003,2479],{"emptyLinePlaceholder":2478},[2426,3005,3007,3010,3012,3014,3016,3018,3020,3022,3024,3026],{"class":2428,"line":3006},30,[2426,3008,3009],{"class":2539},"    handler",[2426,3011,2543],{"class":2539},[2426,3013,2546],{"class":2539},[2426,3015,2549],{"class":2442},[2426,3017,1669],{"class":2552},[2426,3019,2555],{"class":2442},[2426,3021,2558],{"class":2435},[2426,3023,2561],{"class":2442},[2426,3025,2487],{"class":2435},[2426,3027,2566],{"class":2442},[2426,3029,3031,3034,3036,3038,3040,3042],{"class":2428,"line":3030},31,[2426,3032,3033],{"class":2455},"        \"create-user\"",[2426,3035,2561],{"class":2442},[2426,3037,2577],{"class":2455},[2426,3039,2561],{"class":2442},[2426,3041,2582],{"class":2455},[2426,3043,2585],{"class":2442},[2426,3045,3047,3050,3052,3054,3056,3058,3060,3062,3064,3066,3068,3070,3072,3074,3076,3078],{"class":2428,"line":3046},32,[2426,3048,3049],{"class":2431},"        func",[2426,3051,2594],{"class":2442},[2426,3053,2598],{"class":2597},[2426,3055,2602],{"class":2601},[2426,3057,2336],{"class":2435},[2426,3059,2549],{"class":2442},[2426,3061,180],{"class":2435},[2426,3063,2555],{"class":2442},[2426,3065,2558],{"class":2435},[2426,3067,2615],{"class":2442},[2426,3069,2618],{"class":2442},[2426,3071,2621],{"class":2435},[2426,3073,2561],{"class":2442},[2426,3075,2626],{"class":2435},[2426,3077,2629],{"class":2442},[2426,3079,2443],{"class":2442},[2426,3081,3083,3086,3088],{"class":2428,"line":3082},33,[2426,3084,3085],{"class":2601},"            return",[2426,3087,2487],{"class":2435},[2426,3089,2642],{"class":2442},[2426,3091,3093,3096,3098,3100],{"class":2428,"line":3092},34,[2426,3094,3095],{"class":2448},"                ID",[2426,3097,2651],{"class":2442},[2426,3099,2654],{"class":2455},[2426,3101,2585],{"class":2442},[2426,3103,3105,3108,3110,3112,3114,3116,3118,3120],{"class":2428,"line":3104},35,[2426,3106,3107],{"class":2448},"                Name",[2426,3109,2651],{"class":2442},[2426,3111,2667],{"class":2539},[2426,3113,2549],{"class":2442},[2426,3115,2672],{"class":2539},[2426,3117,2549],{"class":2442},[2426,3119,2677],{"class":2539},[2426,3121,2585],{"class":2442},[2426,3123,3125,3128,3130,3132,3134,3136,3138,3140],{"class":2428,"line":3124},36,[2426,3126,3127],{"class":2448},"                Email",[2426,3129,2651],{"class":2442},[2426,3131,2690],{"class":2539},[2426,3133,2549],{"class":2442},[2426,3135,2672],{"class":2539},[2426,3137,2549],{"class":2442},[2426,3139,2699],{"class":2539},[2426,3141,2585],{"class":2442},[2426,3143,3145,3148],{"class":2428,"line":3144},37,[2426,3146,3147],{"class":2442},"            },",[2426,3149,2710],{"class":2431},[2426,3151,3153],{"class":2428,"line":3152},38,[2426,3154,3155],{"class":2442},"        },\n",[2426,3157,3159],{"class":2428,"line":3158},39,[2426,3160,3161],{"class":2442},"    ).\n",[2426,3163,3165,3168,3170,3173],{"class":2428,"line":3164},40,[2426,3166,3167],{"class":2552},"        WithSummary",[2426,3169,2594],{"class":2442},[2426,3171,3172],{"class":2455},"\"Create a new user\"",[2426,3174,3175],{"class":2442},").\n",[2426,3177,3179,3182,3184,3187],{"class":2428,"line":3178},41,[2426,3180,3181],{"class":2552},"        WithTags",[2426,3183,2594],{"class":2442},[2426,3185,3186],{"class":2455},"\"users\"",[2426,3188,3175],{"class":2442},[2426,3190,3192,3195,3197,3201],{"class":2428,"line":3191},42,[2426,3193,3194],{"class":2552},"        WithSuccessStatus",[2426,3196,2594],{"class":2442},[2426,3198,3200],{"class":3199},"sMAmT","201",[2426,3202,3175],{"class":2442},[2426,3204,3206,3209,3211,3213,3215,3217,3219,3221,3223,3225],{"class":2428,"line":3205},43,[2426,3207,3208],{"class":2552},"        WithErrors",[2426,3210,2594],{"class":2442},[2426,3212,2336],{"class":2539},[2426,3214,2549],{"class":2442},[2426,3216,1919],{"class":2539},[2426,3218,2561],{"class":2442},[2426,3220,2546],{"class":2539},[2426,3222,2549],{"class":2442},[2426,3224,1949],{"class":2539},[2426,3226,2743],{"class":2442},[2426,3228,3230],{"class":2428,"line":3229},44,[2426,3231,2479],{"emptyLinePlaceholder":2478},[2426,3233,3235,3237,3239,3241,3243,3245],{"class":2428,"line":3234},45,[2426,3236,2922],{"class":2539},[2426,3238,2549],{"class":2442},[2426,3240,1601],{"class":2552},[2426,3242,2594],{"class":2442},[2426,3244,2540],{"class":2539},[2426,3246,2743],{"class":2442},[2426,3248,3250],{"class":2428,"line":3249},46,[2426,3251,2479],{"emptyLinePlaceholder":2478},[2426,3253,3255],{"class":2428,"line":3254},47,[2426,3256,3257],{"class":2944},"    // OpenAPI spec at /openapi, interactive docs at /docs\n",[2426,3259,3261,3264,3266,3269,3271,3274],{"class":2428,"line":3260},48,[2426,3262,3263],{"class":2539},"    fmt",[2426,3265,2549],{"class":2442},[2426,3267,3268],{"class":2552},"Println",[2426,3270,2594],{"class":2442},[2426,3272,3273],{"class":2455},"\"Server listening on :8080\"",[2426,3275,2743],{"class":2442},[2426,3277,3279,3281,3283,3285,3287,3289,3291,3294,3296,3299],{"class":2428,"line":3278},49,[2426,3280,2922],{"class":2539},[2426,3282,2549],{"class":2442},[2426,3284,1651],{"class":2552},[2426,3286,2594],{"class":2442},[2426,3288,2336],{"class":2539},[2426,3290,2549],{"class":2442},[2426,3292,3293],{"class":2539},"HostAll",[2426,3295,2561],{"class":2442},[2426,3297,3298],{"class":3199}," 8080",[2426,3300,2743],{"class":2442},[2426,3302,3304],{"class":2428,"line":3303},50,[2426,3305,2472],{"class":2442},[2412,3307,3309],{"id":3308},"capabilities","Capabilities",[3311,3312,3313,3329],"table",{},[3314,3315,3316],"thead",{},[3317,3318,3319,3323,3326],"tr",{},[3320,3321,3322],"th",{},"Feature",[3320,3324,3325],{},"Description",[3320,3327,3328],{},"Docs",[3330,3331,3332,3346,3360,3374,3388,3402],"tbody",{},[3317,3333,3334,3338,3341],{},[3335,3336,3337],"td",{},"Type-Safe Handlers",[3335,3339,3340],{},"Generic handlers with compile-time type checking",[3335,3342,3343],{},[2341,3344,346],{"href":3345},"docs/guides/handlers",[3317,3347,3348,3351,3354],{},[3335,3349,3350],{},"Server-Sent Events",[3335,3352,3353],{},"Built-in SSE support for real-time streaming",[3335,3355,3356],{},[2341,3357,3359],{"href":3358},"docs/guides/streaming","Streaming",[3317,3361,3362,3365,3368],{},[3335,3363,3364],{},"Automatic OpenAPI",[3335,3366,3367],{},"Generate OpenAPI 3.1.0 specs from your types",[3335,3369,3370],{},[2341,3371,3373],{"href":3372},"docs/guides/openapi","OpenAPI",[3317,3375,3376,3379,3382],{},[3335,3377,3378],{},"Request Validation",[3335,3380,3381],{},"Struct tag validation with detailed error responses",[3335,3383,3384],{},[2341,3385,3387],{"href":3386},"docs/learn/concepts","Concepts",[3317,3389,3390,3393,3396],{},[3335,3391,3392],{},"Sentinel Errors",[3335,3394,3395],{},"Typed HTTP errors with OpenAPI schema generation",[3335,3397,3398],{},[2341,3399,3401],{"href":3400},"docs/guides/errors","Errors",[3317,3403,3404,3407,3410],{},[3335,3405,3406],{},"Lifecycle Events",[3335,3408,3409],{},"Observable signals for logging, metrics, tracing",[3335,3411,3412],{},[2341,3413,746],{"href":3414},"docs/reference/events",[2412,3416,3418],{"id":3417},"why-rocco","Why rocco?",[3420,3421,3422,3430,3436,3442,3448,3460],"ul",{},[3423,3424,3425,3429],"li",{},[3426,3427,3428],"strong",{},"Type-safe"," — Generic handlers catch errors at compile time, not runtime",[3423,3431,3432,3435],{},[3426,3433,3434],{},"Self-documenting"," — OpenAPI specs generated from the same types that validate requests",[3423,3437,3438,3441],{},[3426,3439,3440],{},"Explicit"," — No magic, no hidden behaviors, no struct tag DSLs for routing",[3423,3443,3444,3447],{},[3426,3445,3446],{},"Chi-powered"," — Built on the battle-tested Chi router with full middleware compatibility",[3423,3449,3450,3453,3454,3459],{},[3426,3451,3452],{},"Observable"," — Lifecycle events via ",[2341,3455,3458],{"href":3456,"rel":3457},"https://github.com/zoobz-io/capitan",[2345],"capitan"," for metrics and tracing",[3423,3461,3462,3465],{},[3426,3463,3464],{},"Streaming-native"," — First-class SSE support with typed event streams",[2412,3467,3469],{"id":3468},"contract-first-by-default","Contract-First by Default",[2338,3471,3472,3473,2549],{},"Rocco enables a pattern: ",[3426,3474,3475],{},"define types once, derive everything else",[2338,3477,3478],{},"Your request and response structs become the single source of truth. From them, rocco derives validation rules, OpenAPI schemas, error contracts, and documentation.",[2338,3480,3481],{},[3426,3482,3483],{},"Define a type:",[2417,3485,3487],{"className":2419,"code":3486,"language":2421,"meta":25,"style":25},"type CreateOrderInput struct {\n    CustomerID string  `json:\"customer_id\" validate:\"required,uuid4\" description:\"Customer UUID\"`\n    Items      []Item  `json:\"items\" validate:\"required,min=1\" description:\"Order line items\"`\n    Total      float64 `json:\"total\" validate:\"required,gt=0\" description:\"Order total in USD\"`\n}\n",[2423,3488,3489,3500,3510,3524,3535],{"__ignoreMap":25},[2426,3490,3491,3493,3496,3498],{"class":2428,"line":9},[2426,3492,2432],{"class":2431},[2426,3494,3495],{"class":2435}," CreateOrderInput",[2426,3497,2439],{"class":2431},[2426,3499,2443],{"class":2442},[2426,3501,3502,3505,3507],{"class":2428,"line":20},[2426,3503,3504],{"class":2448},"    CustomerID",[2426,3506,2464],{"class":2435},[2426,3508,3509],{"class":2455},"  `json:\"customer_id\" validate:\"required,uuid4\" description:\"Customer UUID\"`\n",[2426,3511,3512,3515,3518,3521],{"class":2428,"line":31},[2426,3513,3514],{"class":2448},"    Items",[2426,3516,3517],{"class":2442},"      []",[2426,3519,3520],{"class":2435},"Item",[2426,3522,3523],{"class":2455},"  `json:\"items\" validate:\"required,min=1\" description:\"Order line items\"`\n",[2426,3525,3526,3529,3532],{"class":2428,"line":844},[2426,3527,3528],{"class":2448},"    Total",[2426,3530,3531],{"class":2435},"      float64",[2426,3533,3534],{"class":2455}," `json:\"total\" validate:\"required,gt=0\" description:\"Order total in USD\"`\n",[2426,3536,3537],{"class":2428,"line":2475},[2426,3538,2472],{"class":2442},[2338,3540,3541],{},[3426,3542,3543],{},"Get an OpenAPI schema:",[2417,3545,3549],{"className":3546,"code":3547,"language":3548,"meta":25,"style":25},"language-yaml shiki shiki-themes","CreateOrderInput:\n  type: object\n  required: [customer_id, items, total]\n  properties:\n    customer_id:\n      type: string\n      format: uuid\n      description: Customer UUID\n    items:\n      type: array\n      minItems: 1\n      description: Order line items\n      items:\n        $ref: '#/components/schemas/Item'\n    total:\n      type: number\n      exclusiveMinimum: 0\n      description: Order total in USD\n","yaml",[2423,3550,3551,3559,3570,3595,3602,3609,3619,3629,3639,3646,3655,3665,3674,3681,3691,3698,3707,3717],{"__ignoreMap":25},[2426,3552,3553,3556],{"class":2428,"line":9},[2426,3554,3555],{"class":2431},"CreateOrderInput",[2426,3557,3558],{"class":2799},":\n",[2426,3560,3561,3564,3567],{"class":2428,"line":20},[2426,3562,3563],{"class":2431},"  type",[2426,3565,3566],{"class":2799},": ",[2426,3568,3569],{"class":2455},"object\n",[2426,3571,3572,3575,3578,3581,3584,3587,3589,3592],{"class":2428,"line":31},[2426,3573,3574],{"class":2431},"  required",[2426,3576,3577],{"class":2799},": [",[2426,3579,3580],{"class":2455},"customer_id",[2426,3582,3583],{"class":2799},", ",[2426,3585,3586],{"class":2455},"items",[2426,3588,3583],{"class":2799},[2426,3590,3591],{"class":2455},"total",[2426,3593,3594],{"class":2799},"]\n",[2426,3596,3597,3600],{"class":2428,"line":844},[2426,3598,3599],{"class":2431},"  properties",[2426,3601,3558],{"class":2799},[2426,3603,3604,3607],{"class":2428,"line":2475},[2426,3605,3606],{"class":2431},"    customer_id",[2426,3608,3558],{"class":2799},[2426,3610,3611,3614,3616],{"class":2428,"line":2482},[2426,3612,3613],{"class":2431},"      type",[2426,3615,3566],{"class":2799},[2426,3617,3618],{"class":2455},"string\n",[2426,3620,3621,3624,3626],{"class":2428,"line":2494},[2426,3622,3623],{"class":2431},"      format",[2426,3625,3566],{"class":2799},[2426,3627,3628],{"class":2455},"uuid\n",[2426,3630,3631,3634,3636],{"class":2428,"line":2506},[2426,3632,3633],{"class":2431},"      description",[2426,3635,3566],{"class":2799},[2426,3637,3638],{"class":2455},"Customer UUID\n",[2426,3640,3641,3644],{"class":2428,"line":2516},[2426,3642,3643],{"class":2431},"    items",[2426,3645,3558],{"class":2799},[2426,3647,3648,3650,3652],{"class":2428,"line":2526},[2426,3649,3613],{"class":2431},[2426,3651,3566],{"class":2799},[2426,3653,3654],{"class":2455},"array\n",[2426,3656,3657,3660,3662],{"class":2428,"line":2531},[2426,3658,3659],{"class":2431},"      minItems",[2426,3661,3566],{"class":2799},[2426,3663,3664],{"class":3199},"1\n",[2426,3666,3667,3669,3671],{"class":2428,"line":2536},[2426,3668,3633],{"class":2431},[2426,3670,3566],{"class":2799},[2426,3672,3673],{"class":2455},"Order line items\n",[2426,3675,3676,3679],{"class":2428,"line":2569},[2426,3677,3678],{"class":2431},"      items",[2426,3680,3558],{"class":2799},[2426,3682,3683,3686,3688],{"class":2428,"line":2588},[2426,3684,3685],{"class":2431},"        $ref",[2426,3687,3566],{"class":2799},[2426,3689,3690],{"class":2455},"'#/components/schemas/Item'\n",[2426,3692,3693,3696],{"class":2428,"line":2634},[2426,3694,3695],{"class":2431},"    total",[2426,3697,3558],{"class":2799},[2426,3699,3700,3702,3704],{"class":2428,"line":2645},[2426,3701,3613],{"class":2431},[2426,3703,3566],{"class":2799},[2426,3705,3706],{"class":2455},"number\n",[2426,3708,3709,3712,3714],{"class":2428,"line":2659},[2426,3710,3711],{"class":2431},"      exclusiveMinimum",[2426,3713,3566],{"class":2799},[2426,3715,3716],{"class":3199},"0\n",[2426,3718,3719,3721,3723],{"class":2428,"line":2682},[2426,3720,3633],{"class":2431},[2426,3722,3566],{"class":2799},[2426,3724,3725],{"class":2455},"Order total in USD\n",[2338,3727,3728],{},[3426,3729,3730],{},"Get consistent validation errors:",[2417,3732,3736],{"className":3733,"code":3734,"language":3735,"meta":25,"style":25},"language-json shiki shiki-themes","{\n  \"code\": \"VALIDATION_FAILED\",\n  \"message\": \"validation failed\",\n  \"details\": {\n    \"fields\": [\n      {\"field\": \"customer_id\", \"message\": \"must be a valid UUID\"},\n      {\"field\": \"total\", \"message\": \"must be greater than 0\"}\n    ]\n  }\n}\n","json",[2423,3737,3738,3742,3754,3766,3774,3782,3808,3830,3835,3840],{"__ignoreMap":25},[2426,3739,3740],{"class":2428,"line":9},[2426,3741,2642],{"class":2799},[2426,3743,3744,3747,3749,3752],{"class":2428,"line":20},[2426,3745,3746],{"class":2597},"  \"code\"",[2426,3748,3566],{"class":2799},[2426,3750,3751],{"class":2455},"\"VALIDATION_FAILED\"",[2426,3753,2585],{"class":2799},[2426,3755,3756,3759,3761,3764],{"class":2428,"line":31},[2426,3757,3758],{"class":2597},"  \"message\"",[2426,3760,3566],{"class":2799},[2426,3762,3763],{"class":2455},"\"validation failed\"",[2426,3765,2585],{"class":2799},[2426,3767,3768,3771],{"class":2428,"line":844},[2426,3769,3770],{"class":2597},"  \"details\"",[2426,3772,3773],{"class":2799},": {\n",[2426,3775,3776,3779],{"class":2428,"line":2475},[2426,3777,3778],{"class":2597},"    \"fields\"",[2426,3780,3781],{"class":2799},": [\n",[2426,3783,3784,3787,3790,3792,3795,3797,3800,3802,3805],{"class":2428,"line":2482},[2426,3785,3786],{"class":2799},"      {",[2426,3788,3789],{"class":2597},"\"field\"",[2426,3791,3566],{"class":2799},[2426,3793,3794],{"class":2455},"\"customer_id\"",[2426,3796,3583],{"class":2799},[2426,3798,3799],{"class":2597},"\"message\"",[2426,3801,3566],{"class":2799},[2426,3803,3804],{"class":2455},"\"must be a valid UUID\"",[2426,3806,3807],{"class":2799},"},\n",[2426,3809,3810,3812,3814,3816,3819,3821,3823,3825,3828],{"class":2428,"line":2494},[2426,3811,3786],{"class":2799},[2426,3813,3789],{"class":2597},[2426,3815,3566],{"class":2799},[2426,3817,3818],{"class":2455},"\"total\"",[2426,3820,3583],{"class":2799},[2426,3822,3799],{"class":2597},[2426,3824,3566],{"class":2799},[2426,3826,3827],{"class":2455},"\"must be greater than 0\"",[2426,3829,2472],{"class":2799},[2426,3831,3832],{"class":2428,"line":2506},[2426,3833,3834],{"class":2799},"    ]\n",[2426,3836,3837],{"class":2428,"line":2516},[2426,3838,3839],{"class":2799},"  }\n",[2426,3841,3842],{"class":2428,"line":2526},[2426,3843,2472],{"class":2799},[2338,3845,3846],{},"No separate schema files. No manual sync between code and docs. The types ARE the contract.",[2412,3848,365],{"id":3849},"documentation",[3420,3851,3852],{},[3423,3853,3854,3857],{},[2341,3855,6],{"href":3856},"docs/overview"," — Design philosophy and architecture",[3859,3860,2246],"h3",{"id":3861},"learn",[3420,3863,3864,3870,3875],{},[3423,3865,3866,3869],{},[2341,3867,103],{"href":3868},"docs/learn/quickstart"," — Get started in minutes",[3423,3871,3872,3874],{},[2341,3873,3387],{"href":3386}," — Handlers, requests, validation, errors",[3423,3876,3877,3880],{},[2341,3878,258],{"href":3879},"docs/learn/architecture"," — Internal design and components",[3859,3882,2258],{"id":3883},"guides",[3420,3885,3886,3891,3896,3902,3907,3913],{},[3423,3887,3888,3890],{},[2341,3889,346],{"href":3345}," — Request/response handlers and streaming",[3423,3892,3893,3895],{},[2341,3894,3401],{"href":3400}," — Sentinel errors and custom error types",[3423,3897,3898,3901],{},[2341,3899,1162],{"href":3900},"docs/guides/authentication"," — Identity extraction and middleware",[3423,3903,3904,3906],{},[2341,3905,3373],{"href":3372}," — Schema generation and customization",[3423,3908,3909,3912],{},[2341,3910,619],{"href":3911},"docs/guides/best-practices"," — Patterns and recommendations",[3423,3914,3915,3917],{},[2341,3916,3359],{"href":3358}," — Server-Sent Events",[3859,3919,2275],{"id":3920},"cookbook",[3420,3922,3923,3929,3935,3941],{},[3423,3924,3925,3928],{},[2341,3926,1209],{"href":3927},"docs/cookbook/crud-api"," — Complete REST API example",[3423,3930,3931,3934],{},[2341,3932,1162],{"href":3933},"docs/cookbook/authentication"," — JWT and session patterns",[3423,3936,3937,3940],{},[2341,3938,78],{"href":3939},"docs/cookbook/observability"," — Logging, metrics, tracing",[3423,3942,3943,3947],{},[2341,3944,3946],{"href":3945},"docs/cookbook/realtime","Realtime"," — SSE patterns and use cases",[3859,3949,2288],{"id":3950},"reference",[3420,3952,3953,3960,3966],{},[3423,3954,3955,3959],{},[2341,3956,3958],{"href":3957},"docs/reference/api","API"," — Complete function documentation",[3423,3961,3962,3965],{},[2341,3963,3401],{"href":3964},"docs/reference/errors"," — All sentinel errors and detail types",[3423,3967,3968,3970],{},[2341,3969,746],{"href":3414}," — Lifecycle signals and field keys",[2412,3972,3974],{"id":3973},"contributing","Contributing",[2338,3976,3977,3978,3982],{},"See ",[2341,3979,3981],{"href":3980},"CONTRIBUTING","CONTRIBUTING.md"," for guidelines.",[2412,3984,2388],{"id":3985},"license",[2338,3987,3988,3989,3991],{},"MIT License — see ",[2341,3990,2385],{"href":2385}," for details.",[3993,3994,3995],"style",{},"html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}",{"title":25,"searchDepth":20,"depth":20,"links":3997},[3998,3999,4000,4001,4002,4003,4004,4010,4011],{"id":2414,"depth":20,"text":2415},{"id":2749,"depth":20,"text":2750},{"id":2773,"depth":20,"text":2774},{"id":3308,"depth":20,"text":3309},{"id":3417,"depth":20,"text":3418},{"id":3468,"depth":20,"text":3469},{"id":3849,"depth":20,"text":365,"children":4005},[4006,4007,4008,4009],{"id":3861,"depth":31,"text":2246},{"id":3883,"depth":31,"text":2258},{"id":3920,"depth":31,"text":2275},{"id":3950,"depth":31,"text":2288},{"id":3973,"depth":20,"text":3974},{"id":3985,"depth":20,"text":2388},"md","book-open",{},"/readme",{"title":2329,"description":25},"readme","8rFvJRZ52P4btp332LFTQKFPGgDu-IMekUKy7Ok3Iic",{"id":4020,"title":963,"body":4021,"description":25,"extension":4012,"icon":5312,"meta":5313,"navigation":2478,"path":5314,"seo":5315,"stem":5316,"__hash__":5317},"resources/security.md",{"type":2331,"value":4022,"toc":5290},[4023,4027,4031,4034,4073,4077,4080,4084,4089,4092,4130,4134,4137,4186,4190,4216,4220,4223,4226,4229,4311,4315,4318,4321,4339,4379,4383,4386,4576,4579,4585,4879,4884,4898,4901,4904,4936,4939,4943,4946,4997,5000,5085,5088,5091,5178,5182,5185,5215,5219,5222,5247,5251,5265,5269,5272,5278,5281,5287],[2334,4024,4026],{"id":4025},"security-policy","Security Policy",[2412,4028,4030],{"id":4029},"supported-versions","Supported Versions",[2338,4032,4033],{},"We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating:",[3311,4035,4036,4049],{},[3314,4037,4038],{},[3317,4039,4040,4043,4046],{},[3320,4041,4042],{},"Version",[3320,4044,4045],{},"Supported",[3320,4047,4048],{},"Status",[3330,4050,4051,4062],{},[3317,4052,4053,4056,4059],{},[3335,4054,4055],{},"latest",[3335,4057,4058],{},"✅",[3335,4060,4061],{},"Active development",[3317,4063,4064,4067,4070],{},[3335,4065,4066],{},"\u003C latest",[3335,4068,4069],{},"❌",[3335,4071,4072],{},"Security fixes only for critical issues",[2412,4074,4076],{"id":4075},"reporting-a-vulnerability","Reporting a Vulnerability",[2338,4078,4079],{},"We take the security of rocco seriously. If you have discovered a security vulnerability in this project, please report it responsibly.",[3859,4081,4083],{"id":4082},"how-to-report","How to Report",[2338,4085,4086],{},[3426,4087,4088],{},"Please DO NOT report security vulnerabilities through public GitHub issues.",[2338,4090,4091],{},"Instead, please report them via one of the following methods:",[4093,4094,4095,4118],"ol",{},[3423,4096,4097,4100,4101],{},[3426,4098,4099],{},"GitHub Security Advisories"," (Preferred)",[3420,4102,4103,4112,4115],{},[3423,4104,4105,4106,4111],{},"Go to the ",[2341,4107,4110],{"href":4108,"rel":4109},"https://github.com/zoobz-io/rocco/security",[2345],"Security tab"," of this repository",[3423,4113,4114],{},"Click \"Report a vulnerability\"",[3423,4116,4117],{},"Fill out the form with details about the vulnerability",[3423,4119,4120,4122],{},[3426,4121,2699],{},[3420,4123,4124,4127],{},[3423,4125,4126],{},"Send details to the repository maintainer through GitHub profile contact information",[3423,4128,4129],{},"Use PGP encryption if possible for sensitive details",[3859,4131,4133],{"id":4132},"what-to-include","What to Include",[2338,4135,4136],{},"Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:",[3420,4138,4139,4145,4151,4157,4163,4168,4174,4180],{},[3423,4140,4141,4144],{},[3426,4142,4143],{},"Type of issue"," (e.g., buffer overflow, SQL injection, cross-site scripting, etc.)",[3423,4146,4147,4150],{},[3426,4148,4149],{},"Full paths of source file(s)"," related to the manifestation of the issue",[3423,4152,4153,4156],{},[3426,4154,4155],{},"The location of the affected source code"," (tag/branch/commit or direct URL)",[3423,4158,4159,4162],{},[3426,4160,4161],{},"Any special configuration required"," to reproduce the issue",[3423,4164,4165,4162],{},[3426,4166,4167],{},"Step-by-step instructions",[3423,4169,4170,4173],{},[3426,4171,4172],{},"Proof-of-concept or exploit code"," (if possible)",[3423,4175,4176,4179],{},[3426,4177,4178],{},"Impact of the issue",", including how an attacker might exploit the issue",[3423,4181,4182,4185],{},[3426,4183,4184],{},"Your name and affiliation"," (optional)",[3859,4187,4189],{"id":4188},"what-to-expect","What to Expect",[3420,4191,4192,4198,4204,4210],{},[3423,4193,4194,4197],{},[3426,4195,4196],{},"Acknowledgment",": We will acknowledge receipt of your vulnerability report within 48 hours",[3423,4199,4200,4203],{},[3426,4201,4202],{},"Initial Assessment",": Within 7 days, we will provide an initial assessment of the report",[3423,4205,4206,4209],{},[3426,4207,4208],{},"Resolution Timeline",": We aim to resolve critical issues within 30 days",[3423,4211,4212,4215],{},[3426,4213,4214],{},"Disclosure",": We will coordinate with you on the disclosure timeline",[3859,4217,4219],{"id":4218},"preferred-languages","Preferred Languages",[2338,4221,4222],{},"We prefer all communications to be in English.",[2412,4224,1405],{"id":4225},"security-best-practices",[2338,4227,4228],{},"When using rocco in your applications, we recommend:",[4093,4230,4231,4252,4265,4280,4295],{},[3423,4232,4233,4236],{},[3426,4234,4235],{},"Keep Dependencies Updated",[2417,4237,4239],{"className":2753,"code":4238,"language":2755,"meta":25,"style":25},"go get -u github.com/zoobz-io/rocco\n",[2423,4240,4241],{"__ignoreMap":25},[2426,4242,4243,4245,4247,4250],{"class":2428,"line":9},[2426,4244,2421],{"class":2552},[2426,4246,2764],{"class":2455},[2426,4248,4249],{"class":2431}," -u",[2426,4251,2767],{"class":2455},[3423,4253,4254,4257],{},[3426,4255,4256],{},"Use Context Properly",[3420,4258,4259,4262],{},[3423,4260,4261],{},"Always pass contexts with appropriate timeouts",[3423,4263,4264],{},"Handle context cancellation in your handlers",[3423,4266,4267,4269],{},[3426,4268,457],{},[3420,4270,4271,4274,4277],{},[3423,4272,4273],{},"Declare all sentinel errors with WithErrors",[3423,4275,4276],{},"Never ignore errors from handler processing",[3423,4278,4279],{},"Implement proper fallback mechanisms",[3423,4281,4282,4284],{},[3426,4283,977],{},[3420,4285,4286,4289,4292],{},[3423,4287,4288],{},"Use struct validation tags for all inputs",[3423,4290,4291],{},"Validate both request body and parameters",[3423,4293,4294],{},"Sanitize user inputs before processing",[3423,4296,4297,4300],{},[3426,4298,4299],{},"Resource Management",[3420,4301,4302,4305,4308],{},[3423,4303,4304],{},"Set appropriate timeouts for handlers",[3423,4306,4307],{},"Implement rate limiting middleware",[3423,4309,4310],{},"Use circuit breakers for external services",[2412,4312,4314],{"id":4313},"production-security-guide","Production Security Guide",[3859,4316,967],{"id":4317},"https",[2338,4319,4320],{},"Rocco does not handle TLS directly. In production, use one of these approaches:",[4093,4322,4323,4329],{},[3423,4324,4325,4328],{},[3426,4326,4327],{},"Reverse Proxy"," (Recommended): Use nginx, Caddy, or a cloud load balancer to terminate TLS",[3423,4330,4331,4334,4335,4338],{},[3426,4332,4333],{},"TLS in Go",": Wrap the server with ",[2423,4336,4337],{},"http.ListenAndServeTLS()"," (not directly supported by rocco)",[2417,4340,4342],{"className":2419,"code":4341,"language":2421,"meta":25,"style":25},"// Example: Using behind nginx/Caddy that handles TLS\nengine := rocco.NewEngine().WithAuthenticator(extractIdentity)\n// nginx forwards https://api.example.com -> http://127.0.0.1:8080\n",[2423,4343,4344,4349,4374],{"__ignoreMap":25},[2426,4345,4346],{"class":2428,"line":9},[2426,4347,4348],{"class":2944},"// Example: Using behind nginx/Caddy that handles TLS\n",[2426,4350,4351,4354,4356,4358,4360,4362,4365,4367,4369,4372],{"class":2428,"line":20},[2426,4352,4353],{"class":2539},"engine",[2426,4355,2543],{"class":2539},[2426,4357,2546],{"class":2539},[2426,4359,2549],{"class":2442},[2426,4361,1582],{"class":2552},[2426,4363,4364],{"class":2442},"().",[2426,4366,1591],{"class":2552},[2426,4368,2594],{"class":2442},[2426,4370,4371],{"class":2539},"extractIdentity",[2426,4373,2743],{"class":2442},[2426,4375,4376],{"class":2428,"line":31},[2426,4377,4378],{"class":2944},"// nginx forwards https://api.example.com -> http://127.0.0.1:8080\n",[3859,4380,4382],{"id":4381},"cors","CORS",[2338,4384,4385],{},"Rocco doesn't include CORS middleware. Use Chi's cors middleware:",[2417,4387,4389],{"className":2419,"code":4388,"language":2421,"meta":25,"style":25},"import \"github.com/go-chi/cors\"\n\nengine := rocco.NewEngine()\nengine.WithMiddleware(cors.Handler(cors.Options{\n    AllowedOrigins:   []string{\"https://example.com\"},\n    AllowedMethods:   []string{\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\"},\n    AllowedHeaders:   []string{\"Accept\", \"Authorization\", \"Content-Type\"},\n    ExposedHeaders:   []string{\"Link\"},\n    AllowCredentials: true,\n    MaxAge:           300,\n}))\n",[2423,4390,4391,4398,4402,4416,4443,4464,4501,4529,4547,4559,4571],{"__ignoreMap":25},[2426,4392,4393,4395],{"class":2428,"line":9},[2426,4394,2796],{"class":2431},[2426,4396,4397],{"class":2455}," \"github.com/go-chi/cors\"\n",[2426,4399,4400],{"class":2428,"line":20},[2426,4401,2479],{"emptyLinePlaceholder":2478},[2426,4403,4404,4406,4408,4410,4412,4414],{"class":2428,"line":31},[2426,4405,4353],{"class":2539},[2426,4407,2543],{"class":2539},[2426,4409,2546],{"class":2539},[2426,4411,2549],{"class":2442},[2426,4413,1582],{"class":2552},[2426,4415,2933],{"class":2442},[2426,4417,4418,4420,4422,4424,4426,4428,4430,4432,4434,4436,4438,4441],{"class":2428,"line":844},[2426,4419,4353],{"class":2539},[2426,4421,2549],{"class":2442},[2426,4423,1596],{"class":2552},[2426,4425,2594],{"class":2442},[2426,4427,4381],{"class":2539},[2426,4429,2549],{"class":2442},[2426,4431,165],{"class":2552},[2426,4433,2594],{"class":2442},[2426,4435,4381],{"class":2435},[2426,4437,2549],{"class":2442},[2426,4439,4440],{"class":2435},"Options",[2426,4442,2642],{"class":2442},[2426,4444,4445,4448,4450,4453,4456,4459,4462],{"class":2428,"line":2475},[2426,4446,4447],{"class":2448},"    AllowedOrigins",[2426,4449,2651],{"class":2442},[2426,4451,4452],{"class":2442},"   []",[2426,4454,4455],{"class":2435},"string",[2426,4457,4458],{"class":2442},"{",[2426,4460,4461],{"class":2455},"\"https://example.com\"",[2426,4463,3807],{"class":2442},[2426,4465,4466,4469,4471,4473,4475,4477,4480,4482,4484,4486,4489,4491,4494,4496,4499],{"class":2428,"line":2482},[2426,4467,4468],{"class":2448},"    AllowedMethods",[2426,4470,2651],{"class":2442},[2426,4472,4452],{"class":2442},[2426,4474,4455],{"class":2435},[2426,4476,4458],{"class":2442},[2426,4478,4479],{"class":2455},"\"GET\"",[2426,4481,2561],{"class":2442},[2426,4483,2577],{"class":2455},[2426,4485,2561],{"class":2442},[2426,4487,4488],{"class":2455}," \"PUT\"",[2426,4490,2561],{"class":2442},[2426,4492,4493],{"class":2455}," \"DELETE\"",[2426,4495,2561],{"class":2442},[2426,4497,4498],{"class":2455}," \"OPTIONS\"",[2426,4500,3807],{"class":2442},[2426,4502,4503,4506,4508,4510,4512,4514,4517,4519,4522,4524,4527],{"class":2428,"line":2494},[2426,4504,4505],{"class":2448},"    AllowedHeaders",[2426,4507,2651],{"class":2442},[2426,4509,4452],{"class":2442},[2426,4511,4455],{"class":2435},[2426,4513,4458],{"class":2442},[2426,4515,4516],{"class":2455},"\"Accept\"",[2426,4518,2561],{"class":2442},[2426,4520,4521],{"class":2455}," \"Authorization\"",[2426,4523,2561],{"class":2442},[2426,4525,4526],{"class":2455}," \"Content-Type\"",[2426,4528,3807],{"class":2442},[2426,4530,4531,4534,4536,4538,4540,4542,4545],{"class":2428,"line":2506},[2426,4532,4533],{"class":2448},"    ExposedHeaders",[2426,4535,2651],{"class":2442},[2426,4537,4452],{"class":2442},[2426,4539,4455],{"class":2435},[2426,4541,4458],{"class":2442},[2426,4543,4544],{"class":2455},"\"Link\"",[2426,4546,3807],{"class":2442},[2426,4548,4549,4552,4554,4557],{"class":2428,"line":2516},[2426,4550,4551],{"class":2448},"    AllowCredentials",[2426,4553,2651],{"class":2442},[2426,4555,4556],{"class":2431}," true",[2426,4558,2585],{"class":2442},[2426,4560,4561,4564,4566,4569],{"class":2428,"line":2526},[2426,4562,4563],{"class":2448},"    MaxAge",[2426,4565,2651],{"class":2442},[2426,4567,4568],{"class":3199},"           300",[2426,4570,2585],{"class":2442},[2426,4572,4573],{"class":2428,"line":2531},[2426,4574,4575],{"class":2442},"}))\n",[3859,4577,672],{"id":4578},"identity-extraction",[2338,4580,4581,4582,4584],{},"The ",[2423,4583,4371],{}," callback is critical for security. Guidelines:",[2417,4586,4588],{"className":2419,"code":4587,"language":2421,"meta":25,"style":25},"func extractIdentity(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    // 1. Extract token from header\n    token := r.Header.Get(\"Authorization\")\n    if token == \"\" {\n        return nil, errors.New(\"missing authorization header\")\n    }\n\n    // 2. Validate token (JWT verification, session lookup, etc.)\n    claims, err := validateToken(strings.TrimPrefix(token, \"Bearer \"))\n    if err != nil {\n        return nil, err // Returns 401 Unauthorized\n    }\n\n    // 3. Return identity with scopes/roles for authorization\n    return &UserIdentity{\n        ID:     claims.Subject,\n        Scopes: claims.Scopes,\n        Roles:  claims.Roles,\n    }, nil\n}\n",[2423,4589,4590,4642,4647,4673,4689,4713,4718,4722,4727,4765,4778,4791,4795,4799,4804,4817,4834,4851,4868,4875],{"__ignoreMap":25},[2426,4591,4592,4594,4597,4599,4602,4605,4607,4610,4612,4615,4617,4620,4622,4624,4626,4628,4630,4632,4634,4636,4638,4640],{"class":2428,"line":9},[2426,4593,2908],{"class":2431},[2426,4595,4596],{"class":2552}," extractIdentity",[2426,4598,2594],{"class":2442},[2426,4600,4601],{"class":2597},"ctx",[2426,4603,4604],{"class":2435}," context",[2426,4606,2549],{"class":2442},[2426,4608,4609],{"class":2435},"Context",[2426,4611,2561],{"class":2442},[2426,4613,4614],{"class":2597}," r",[2426,4616,2602],{"class":2601},[2426,4618,4619],{"class":2435},"http",[2426,4621,2549],{"class":2442},[2426,4623,180],{"class":2435},[2426,4625,2629],{"class":2442},[2426,4627,2618],{"class":2442},[2426,4629,2336],{"class":2435},[2426,4631,2549],{"class":2442},[2426,4633,245],{"class":2435},[2426,4635,2561],{"class":2442},[2426,4637,2626],{"class":2435},[2426,4639,2629],{"class":2442},[2426,4641,2443],{"class":2442},[2426,4643,4644],{"class":2428,"line":20},[2426,4645,4646],{"class":2944},"    // 1. Extract token from header\n",[2426,4648,4649,4652,4654,4656,4658,4661,4663,4666,4668,4671],{"class":2428,"line":31},[2426,4650,4651],{"class":2539},"    token",[2426,4653,2543],{"class":2539},[2426,4655,4614],{"class":2539},[2426,4657,2549],{"class":2442},[2426,4659,4660],{"class":2539},"Header",[2426,4662,2549],{"class":2442},[2426,4664,4665],{"class":2552},"Get",[2426,4667,2594],{"class":2442},[2426,4669,4670],{"class":2455},"\"Authorization\"",[2426,4672,2743],{"class":2442},[2426,4674,4675,4678,4681,4684,4687],{"class":2428,"line":844},[2426,4676,4677],{"class":2601},"    if",[2426,4679,4680],{"class":2539}," token",[2426,4682,4683],{"class":2601}," ==",[2426,4685,4686],{"class":2455}," \"\"",[2426,4688,2443],{"class":2442},[2426,4690,4691,4693,4696,4698,4701,4703,4706,4708,4711],{"class":2428,"line":2475},[2426,4692,2637],{"class":2601},[2426,4694,4695],{"class":2431}," nil",[2426,4697,2561],{"class":2442},[2426,4699,4700],{"class":2539}," errors",[2426,4702,2549],{"class":2442},[2426,4704,4705],{"class":2552},"New",[2426,4707,2594],{"class":2442},[2426,4709,4710],{"class":2455},"\"missing authorization header\"",[2426,4712,2743],{"class":2442},[2426,4714,4715],{"class":2428,"line":2482},[2426,4716,4717],{"class":2442},"    }\n",[2426,4719,4720],{"class":2428,"line":2494},[2426,4721,2479],{"emptyLinePlaceholder":2478},[2426,4723,4724],{"class":2428,"line":2506},[2426,4725,4726],{"class":2944},"    // 2. Validate token (JWT verification, session lookup, etc.)\n",[2426,4728,4729,4732,4734,4737,4739,4742,4744,4747,4749,4752,4754,4757,4759,4762],{"class":2428,"line":2516},[2426,4730,4731],{"class":2539},"    claims",[2426,4733,2561],{"class":2442},[2426,4735,4736],{"class":2539}," err",[2426,4738,2543],{"class":2539},[2426,4740,4741],{"class":2552}," validateToken",[2426,4743,2594],{"class":2442},[2426,4745,4746],{"class":2539},"strings",[2426,4748,2549],{"class":2442},[2426,4750,4751],{"class":2552},"TrimPrefix",[2426,4753,2594],{"class":2442},[2426,4755,4756],{"class":2539},"token",[2426,4758,2561],{"class":2442},[2426,4760,4761],{"class":2455}," \"Bearer \"",[2426,4763,4764],{"class":2442},"))\n",[2426,4766,4767,4769,4771,4774,4776],{"class":2428,"line":2526},[2426,4768,4677],{"class":2601},[2426,4770,4736],{"class":2539},[2426,4772,4773],{"class":2601}," !=",[2426,4775,4695],{"class":2431},[2426,4777,2443],{"class":2442},[2426,4779,4780,4782,4784,4786,4788],{"class":2428,"line":2531},[2426,4781,2637],{"class":2601},[2426,4783,4695],{"class":2431},[2426,4785,2561],{"class":2442},[2426,4787,4736],{"class":2539},[2426,4789,4790],{"class":2944}," // Returns 401 Unauthorized\n",[2426,4792,4793],{"class":2428,"line":2536},[2426,4794,4717],{"class":2442},[2426,4796,4797],{"class":2428,"line":2569},[2426,4798,2479],{"emptyLinePlaceholder":2478},[2426,4800,4801],{"class":2428,"line":2588},[2426,4802,4803],{"class":2944},"    // 3. Return identity with scopes/roles for authorization\n",[2426,4805,4806,4809,4812,4815],{"class":2428,"line":2634},[2426,4807,4808],{"class":2601},"    return",[2426,4810,4811],{"class":2601}," &",[2426,4813,4814],{"class":2435},"UserIdentity",[2426,4816,2642],{"class":2442},[2426,4818,4819,4822,4824,4827,4829,4832],{"class":2428,"line":2645},[2426,4820,4821],{"class":2448},"        ID",[2426,4823,2651],{"class":2442},[2426,4825,4826],{"class":2539},"     claims",[2426,4828,2549],{"class":2442},[2426,4830,4831],{"class":2539},"Subject",[2426,4833,2585],{"class":2442},[2426,4835,4836,4839,4841,4844,4846,4849],{"class":2428,"line":2659},[2426,4837,4838],{"class":2448},"        Scopes",[2426,4840,2651],{"class":2442},[2426,4842,4843],{"class":2539}," claims",[2426,4845,2549],{"class":2442},[2426,4847,4848],{"class":2539},"Scopes",[2426,4850,2585],{"class":2442},[2426,4852,4853,4856,4858,4861,4863,4866],{"class":2428,"line":2682},[2426,4854,4855],{"class":2448},"        Roles",[2426,4857,2651],{"class":2442},[2426,4859,4860],{"class":2539},"  claims",[2426,4862,2549],{"class":2442},[2426,4864,4865],{"class":2539},"Roles",[2426,4867,2585],{"class":2442},[2426,4869,4870,4873],{"class":2428,"line":2704},[2426,4871,4872],{"class":2442},"    },",[2426,4874,2710],{"class":2431},[2426,4876,4877],{"class":2428,"line":2713},[2426,4878,2472],{"class":2442},[2338,4880,4881],{},[3426,4882,4883],{},"Important:",[3420,4885,4886,4889,4892,4895],{},[3423,4887,4888],{},"Never trust client-provided identity claims without verification",[3423,4890,4891],{},"Use constant-time comparison for token validation",[3423,4893,4894],{},"Set reasonable token expiration times",[3423,4896,4897],{},"Log authentication failures for security monitoring",[3859,4899,972],{"id":4900},"request-size-limits",[2338,4902,4903],{},"Rocco enforces a 10MB default body size limit. Adjust per-handler:",[2417,4905,4907],{"className":2419,"code":4906,"language":2421,"meta":25,"style":25},"handler.WithMaxBodySize(1 * 1024 * 1024) // 1MB limit\n",[2423,4908,4909],{"__ignoreMap":25},[2426,4910,4911,4913,4915,4917,4919,4922,4924,4927,4929,4931,4933],{"class":2428,"line":9},[2426,4912,2540],{"class":2539},[2426,4914,2549],{"class":2442},[2426,4916,1723],{"class":2552},[2426,4918,2594],{"class":2442},[2426,4920,4921],{"class":3199},"1",[2426,4923,2602],{"class":2539},[2426,4925,4926],{"class":3199}," 1024",[2426,4928,2602],{"class":2539},[2426,4930,4926],{"class":3199},[2426,4932,2629],{"class":2442},[2426,4934,4935],{"class":2944}," // 1MB limit\n",[2338,4937,4938],{},"Requests exceeding the limit return 413 Payload Too Large.",[3859,4940,4942],{"id":4941},"rate-limiting","Rate Limiting",[2338,4944,4945],{},"Implement rate limiting at the middleware level:",[2417,4947,4949],{"className":2419,"code":4948,"language":2421,"meta":25,"style":25},"import \"github.com/go-chi/httprate\"\n\nengine.WithMiddleware(httprate.LimitByIP(100, time.Minute))\n",[2423,4950,4951,4958,4962],{"__ignoreMap":25},[2426,4952,4953,4955],{"class":2428,"line":9},[2426,4954,2796],{"class":2431},[2426,4956,4957],{"class":2455}," \"github.com/go-chi/httprate\"\n",[2426,4959,4960],{"class":2428,"line":20},[2426,4961,2479],{"emptyLinePlaceholder":2478},[2426,4963,4964,4966,4968,4970,4972,4975,4977,4980,4982,4985,4987,4990,4992,4995],{"class":2428,"line":31},[2426,4965,4353],{"class":2539},[2426,4967,2549],{"class":2442},[2426,4969,1596],{"class":2552},[2426,4971,2594],{"class":2442},[2426,4973,4974],{"class":2539},"httprate",[2426,4976,2549],{"class":2442},[2426,4978,4979],{"class":2552},"LimitByIP",[2426,4981,2594],{"class":2442},[2426,4983,4984],{"class":3199},"100",[2426,4986,2561],{"class":2442},[2426,4988,4989],{"class":2539}," time",[2426,4991,2549],{"class":2442},[2426,4993,4994],{"class":2539},"Minute",[2426,4996,4764],{"class":2442},[2338,4998,4999],{},"Or use rocco's built-in usage limits for authenticated routes:",[2417,5001,5003],{"className":2419,"code":5002,"language":2421,"meta":25,"style":25},"handler.WithUsageLimit(\"api_calls\", func(id rocco.Identity) int {\n    if id.HasRole(\"premium\") {\n        return 10000\n    }\n    return 100\n})\n",[2423,5004,5005,5041,5062,5069,5073,5080],{"__ignoreMap":25},[2426,5006,5007,5009,5011,5013,5015,5018,5020,5023,5025,5028,5030,5032,5034,5036,5039],{"class":2428,"line":9},[2426,5008,2540],{"class":2539},[2426,5010,2549],{"class":2442},[2426,5012,1756],{"class":2552},[2426,5014,2594],{"class":2442},[2426,5016,5017],{"class":2455},"\"api_calls\"",[2426,5019,2561],{"class":2442},[2426,5021,5022],{"class":2431}," func",[2426,5024,2594],{"class":2442},[2426,5026,5027],{"class":2597},"id",[2426,5029,2546],{"class":2435},[2426,5031,2549],{"class":2442},[2426,5033,245],{"class":2435},[2426,5035,2629],{"class":2442},[2426,5037,5038],{"class":2435}," int",[2426,5040,2443],{"class":2442},[2426,5042,5043,5045,5048,5050,5053,5055,5058,5060],{"class":2428,"line":20},[2426,5044,4677],{"class":2601},[2426,5046,5047],{"class":2539}," id",[2426,5049,2549],{"class":2442},[2426,5051,5052],{"class":2552},"HasRole",[2426,5054,2594],{"class":2442},[2426,5056,5057],{"class":2455},"\"premium\"",[2426,5059,2629],{"class":2442},[2426,5061,2443],{"class":2442},[2426,5063,5064,5066],{"class":2428,"line":31},[2426,5065,2637],{"class":2601},[2426,5067,5068],{"class":3199}," 10000\n",[2426,5070,5071],{"class":2428,"line":844},[2426,5072,4717],{"class":2442},[2426,5074,5075,5077],{"class":2428,"line":2475},[2426,5076,4808],{"class":2601},[2426,5078,5079],{"class":3199}," 100\n",[2426,5081,5082],{"class":2428,"line":2482},[2426,5083,5084],{"class":2442},"})\n",[3859,5086,982],{"id":5087},"sql-injection-prevention",[2338,5089,5090],{},"Rocco validates JSON input but doesn't protect against SQL injection. Always use parameterized queries:",[2417,5092,5094],{"className":2419,"code":5093,"language":2421,"meta":25,"style":25},"// WRONG - vulnerable\ndb.Query(\"SELECT * FROM users WHERE id = \" + req.Params.Path[\"id\"])\n\n// CORRECT - parameterized\ndb.Query(\"SELECT * FROM users WHERE id = $1\", req.Params.Path[\"id\"])\n",[2423,5095,5096,5101,5138,5142,5147],{"__ignoreMap":25},[2426,5097,5098],{"class":2428,"line":9},[2426,5099,5100],{"class":2944},"// WRONG - vulnerable\n",[2426,5102,5103,5106,5108,5111,5113,5116,5119,5121,5123,5125,5127,5130,5132,5135],{"class":2428,"line":20},[2426,5104,5105],{"class":2539},"db",[2426,5107,2549],{"class":2442},[2426,5109,5110],{"class":2552},"Query",[2426,5112,2594],{"class":2442},[2426,5114,5115],{"class":2455},"\"SELECT * FROM users WHERE id = \"",[2426,5117,5118],{"class":2539}," +",[2426,5120,2690],{"class":2539},[2426,5122,2549],{"class":2442},[2426,5124,1804],{"class":2539},[2426,5126,2549],{"class":2442},[2426,5128,5129],{"class":2539},"Path",[2426,5131,2555],{"class":2442},[2426,5133,5134],{"class":2455},"\"id\"",[2426,5136,5137],{"class":2442},"])\n",[2426,5139,5140],{"class":2428,"line":31},[2426,5141,2479],{"emptyLinePlaceholder":2478},[2426,5143,5144],{"class":2428,"line":844},[2426,5145,5146],{"class":2944},"// CORRECT - parameterized\n",[2426,5148,5149,5151,5153,5155,5157,5160,5162,5164,5166,5168,5170,5172,5174,5176],{"class":2428,"line":2475},[2426,5150,5105],{"class":2539},[2426,5152,2549],{"class":2442},[2426,5154,5110],{"class":2552},[2426,5156,2594],{"class":2442},[2426,5158,5159],{"class":2455},"\"SELECT * FROM users WHERE id = $1\"",[2426,5161,2561],{"class":2442},[2426,5163,2690],{"class":2539},[2426,5165,2549],{"class":2442},[2426,5167,1804],{"class":2539},[2426,5169,2549],{"class":2442},[2426,5171,5129],{"class":2539},[2426,5173,2555],{"class":2442},[2426,5175,5134],{"class":2455},[2426,5177,5137],{"class":2442},[2412,5179,5181],{"id":5180},"security-features","Security Features",[2338,5183,5184],{},"rocco includes several built-in security features:",[3420,5186,5187,5193,5199,5205,5210],{},[3423,5188,5189,5192],{},[3426,5190,5191],{},"Type Safety",": Generic types prevent type confusion attacks",[3423,5194,5195,5198],{},[3426,5196,5197],{},"Context Support",": Built-in cancellation and timeout support",[3423,5200,5201,5204],{},[3426,5202,5203],{},"Error Isolation",": Sentinel errors are properly tracked and reported",[3423,5206,5207,5209],{},[3426,5208,977],{},": Automatic struct validation with detailed error messages",[3423,5211,5212,5214],{},[3426,5213,78],{},": Built-in metrics and tracing for security monitoring",[2412,5216,5218],{"id":5217},"automated-security-scanning","Automated Security Scanning",[2338,5220,5221],{},"This project uses:",[3420,5223,5224,5229,5235,5241],{},[3423,5225,5226,5228],{},[3426,5227,2373],{},": GitHub's semantic code analysis for security vulnerabilities",[3423,5230,5231,5234],{},[3426,5232,5233],{},"Dependabot",": Automated dependency updates",[3423,5236,5237,5240],{},[3426,5238,5239],{},"golangci-lint",": Static analysis including security linters",[3423,5242,5243,5246],{},[3426,5244,5245],{},"Codecov",": Coverage tracking to ensure security-critical code is tested",[2412,5248,5250],{"id":5249},"vulnerability-disclosure-policy","Vulnerability Disclosure Policy",[3420,5252,5253,5256,5259,5262],{},[3423,5254,5255],{},"Security vulnerabilities will be disclosed via GitHub Security Advisories",[3423,5257,5258],{},"We follow a 90-day disclosure timeline for non-critical issues",[3423,5260,5261],{},"Critical vulnerabilities may be disclosed sooner after patches are available",[3423,5263,5264],{},"We will credit reporters who follow responsible disclosure practices",[2412,5266,5268],{"id":5267},"credits","Credits",[2338,5270,5271],{},"We thank the following individuals for responsibly disclosing security issues:",[2338,5273,5274],{},[5275,5276,5277],"em",{},"This list is currently empty. Be the first to help improve our security!",[5279,5280],"hr",{},[2338,5282,5283,5286],{},[3426,5284,5285],{},"Last Updated",": 2025-10-15",[3993,5288,5289],{},"html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}",{"title":25,"searchDepth":20,"depth":20,"links":5291},[5292,5293,5299,5300,5308,5309,5310,5311],{"id":4029,"depth":20,"text":4030},{"id":4075,"depth":20,"text":4076,"children":5294},[5295,5296,5297,5298],{"id":4082,"depth":31,"text":4083},{"id":4132,"depth":31,"text":4133},{"id":4188,"depth":31,"text":4189},{"id":4218,"depth":31,"text":4219},{"id":4225,"depth":20,"text":1405},{"id":4313,"depth":20,"text":4314,"children":5301},[5302,5303,5304,5305,5306,5307],{"id":4317,"depth":31,"text":967},{"id":4381,"depth":31,"text":4382},{"id":4578,"depth":31,"text":672},{"id":4900,"depth":31,"text":972},{"id":4941,"depth":31,"text":4942},{"id":5087,"depth":31,"text":982},{"id":5180,"depth":20,"text":5181},{"id":5217,"depth":20,"text":5218},{"id":5249,"depth":20,"text":5250},{"id":5267,"depth":20,"text":5268},"shield",{},"/security",{"title":963,"description":25},"security","eV0I-cNuPnXHEcaZjTArDWOkO7t6R3Lf6UKy0idEk4g",{"id":5319,"title":3974,"body":5320,"description":5328,"extension":4012,"icon":2423,"meta":5900,"navigation":2478,"path":5901,"seo":5902,"stem":3973,"__hash__":5903},"resources/contributing.md",{"type":2331,"value":5321,"toc":5874},[5322,5326,5329,5333,5336,5340,5378,5382,5386,5404,5407,5423,5425,5436,5440,5444,5458,5462,5473,5477,5482,5485,5499,5503,5506,5520,5524,5527,5541,5545,5573,5576,5579,5594,5597,5613,5616,5632,5636,5644,5648,5651,5695,5699,5703,5706,5717,5720,5734,5738,5741,5769,5773,5798,5802,5829,5835,5839,5842,5853,5857,5868,5871],[2334,5323,5325],{"id":5324},"contributing-to-rocco","Contributing to rocco",[2338,5327,5328],{},"Thank you for your interest in contributing to rocco! This guide will help you get started.",[2412,5330,5332],{"id":5331},"code-of-conduct","Code of Conduct",[2338,5334,5335],{},"By participating in this project, you agree to maintain a respectful and inclusive environment for all contributors.",[2412,5337,5339],{"id":5338},"getting-started","Getting Started",[4093,5341,5342,5345,5351,5357,5360,5366,5369,5375],{},[3423,5343,5344],{},"Fork the repository",[3423,5346,5347,5348],{},"Clone your fork: ",[2423,5349,5350],{},"git clone https://github.com/yourusername/rocco.git",[3423,5352,5353,5354],{},"Create a feature branch: ",[2423,5355,5356],{},"git checkout -b feature/your-feature-name",[3423,5358,5359],{},"Make your changes",[3423,5361,5362,5363],{},"Run tests: ",[2423,5364,5365],{},"go test ./...",[3423,5367,5368],{},"Commit your changes with a descriptive message",[3423,5370,5371,5372],{},"Push to your fork: ",[2423,5373,5374],{},"git push origin feature/your-feature-name",[3423,5376,5377],{},"Create a Pull Request",[2412,5379,5381],{"id":5380},"development-guidelines","Development Guidelines",[3859,5383,5385],{"id":5384},"code-style","Code Style",[3420,5387,5388,5391,5398,5401],{},[3423,5389,5390],{},"Follow standard Go conventions",[3423,5392,5393,5394,5397],{},"Run ",[2423,5395,5396],{},"go fmt"," before committing",[3423,5399,5400],{},"Add comments for exported functions and types",[3423,5402,5403],{},"Keep functions small and focused",[3859,5405,1086],{"id":5406},"testing",[3420,5408,5409,5412,5417,5420],{},[3423,5410,5411],{},"Write tests for new functionality",[3423,5413,5414,5415],{},"Ensure all tests pass: ",[2423,5416,5365],{},[3423,5418,5419],{},"Include benchmarks for performance-critical code",[3423,5421,5422],{},"Aim for high test coverage (90%+)",[3859,5424,365],{"id":3849},[3420,5426,5427,5430,5433],{},[3423,5428,5429],{},"Update documentation for API changes",[3423,5431,5432],{},"Add examples for new features",[3423,5434,5435],{},"Keep doc comments clear and concise",[2412,5437,5439],{"id":5438},"types-of-contributions","Types of Contributions",[3859,5441,5443],{"id":5442},"bug-reports","Bug Reports",[3420,5445,5446,5449,5452,5455],{},[3423,5447,5448],{},"Use GitHub Issues",[3423,5450,5451],{},"Include minimal reproduction code",[3423,5453,5454],{},"Describe expected vs actual behavior",[3423,5456,5457],{},"Include Go version and OS",[3859,5459,5461],{"id":5460},"feature-requests","Feature Requests",[3420,5463,5464,5467,5470],{},[3423,5465,5466],{},"Open an issue for discussion first",[3423,5468,5469],{},"Explain the use case",[3423,5471,5472],{},"Consider backwards compatibility",[3859,5474,5476],{"id":5475},"code-contributions","Code Contributions",[5478,5479,5481],"h4",{"id":5480},"adding-handlers","Adding Handlers",[2338,5483,5484],{},"New handlers should:",[3420,5486,5487,5490,5493,5496],{},[3423,5488,5489],{},"Use generic types for type safety",[3423,5491,5492],{},"Implement proper error handling with sentinel errors",[3423,5494,5495],{},"Include validation using struct tags",[3423,5497,5498],{},"Add comprehensive tests",[5478,5500,5502],{"id":5501},"adding-middleware","Adding Middleware",[2338,5504,5505],{},"New middleware should:",[3420,5507,5508,5511,5514,5517],{},[3423,5509,5510],{},"Follow Chi middleware conventions",[3423,5512,5513],{},"Handle context cancellation properly",[3423,5515,5516],{},"Include tests for error cases",[3423,5518,5519],{},"Document behavior clearly",[5478,5521,5523],{"id":5522},"examples","Examples",[2338,5525,5526],{},"New examples should:",[3420,5528,5529,5532,5535,5538],{},[3423,5530,5531],{},"Solve a real-world problem",[3423,5533,5534],{},"Include tests and benchmarks",[3423,5536,5537],{},"Have a descriptive README",[3423,5539,5540],{},"Follow the existing structure",[2412,5542,5544],{"id":5543},"pull-request-process","Pull Request Process",[4093,5546,5547,5553,5558,5563,5568],{},[3423,5548,5549,5552],{},[3426,5550,5551],{},"Keep PRs focused"," - One feature/fix per PR",[3423,5554,5555],{},[3426,5556,5557],{},"Write descriptive commit messages",[3423,5559,5560],{},[3426,5561,5562],{},"Update tests and documentation",[3423,5564,5565],{},[3426,5566,5567],{},"Ensure CI passes",[3423,5569,5570],{},[3426,5571,5572],{},"Respond to review feedback",[2412,5574,1086],{"id":5575},"testing-1",[2338,5577,5578],{},"Run the full test suite:",[2417,5580,5582],{"className":2753,"code":5581,"language":2755,"meta":25,"style":25},"go test ./...\n",[2423,5583,5584],{"__ignoreMap":25},[2426,5585,5586,5588,5591],{"class":2428,"line":9},[2426,5587,2421],{"class":2552},[2426,5589,5590],{"class":2455}," test",[2426,5592,5593],{"class":2455}," ./...\n",[2338,5595,5596],{},"Run with race detection:",[2417,5598,5600],{"className":2753,"code":5599,"language":2755,"meta":25,"style":25},"go test -race ./...\n",[2423,5601,5602],{"__ignoreMap":25},[2426,5603,5604,5606,5608,5611],{"class":2428,"line":9},[2426,5605,2421],{"class":2552},[2426,5607,5590],{"class":2455},[2426,5609,5610],{"class":2431}," -race",[2426,5612,5593],{"class":2455},[2338,5614,5615],{},"Run benchmarks:",[2417,5617,5619],{"className":2753,"code":5618,"language":2755,"meta":25,"style":25},"go test -bench=. ./...\n",[2423,5620,5621],{"__ignoreMap":25},[2426,5622,5623,5625,5627,5630],{"class":2428,"line":9},[2426,5624,2421],{"class":2552},[2426,5626,5590],{"class":2455},[2426,5628,5629],{"class":2431}," -bench=.",[2426,5631,5593],{"class":2455},[2412,5633,5635],{"id":5634},"project-structure","Project Structure",[2417,5637,5642],{"className":5638,"code":5640,"language":5641},[5639],"language-text","rocco/\n├── *.go              # Core library files\n├── *_test.go         # Tests\n├── examples/         # Example implementations\n│   └── */           # Individual examples\n└── docs/            # Documentation\n","text",[2423,5643,5640],{"__ignoreMap":25},[2412,5645,5647],{"id":5646},"commit-messages","Commit Messages",[2338,5649,5650],{},"Follow conventional commits:",[3420,5652,5653,5659,5665,5671,5677,5683,5689],{},[3423,5654,5655,5658],{},[2423,5656,5657],{},"feat:"," New feature",[3423,5660,5661,5664],{},[2423,5662,5663],{},"fix:"," Bug fix",[3423,5666,5667,5670],{},[2423,5668,5669],{},"docs:"," Documentation changes",[3423,5672,5673,5676],{},[2423,5674,5675],{},"test:"," Test additions/changes",[3423,5678,5679,5682],{},[2423,5680,5681],{},"refactor:"," Code refactoring",[3423,5684,5685,5688],{},[2423,5686,5687],{},"perf:"," Performance improvements",[3423,5690,5691,5694],{},[2423,5692,5693],{},"chore:"," Maintenance tasks",[2412,5696,5698],{"id":5697},"release-process","Release Process",[3859,5700,5702],{"id":5701},"automated-releases","Automated Releases",[2338,5704,5705],{},"This project uses automated release versioning. To create a release:",[4093,5707,5708,5711,5714],{},[3423,5709,5710],{},"Go to Actions → Release → Run workflow",[3423,5712,5713],{},"Leave \"Version override\" empty for automatic version inference",[3423,5715,5716],{},"Click \"Run workflow\"",[2338,5718,5719],{},"The system will:",[3420,5721,5722,5725,5728,5731],{},[3423,5723,5724],{},"Automatically determine the next version from conventional commits",[3423,5726,5727],{},"Create a git tag",[3423,5729,5730],{},"Generate release notes via GoReleaser",[3423,5732,5733],{},"Publish the release to GitHub",[3859,5735,5737],{"id":5736},"manual-release-legacy","Manual Release (Legacy)",[2338,5739,5740],{},"You can still create releases manually:",[2417,5742,5744],{"className":2753,"code":5743,"language":2755,"meta":25,"style":25},"git tag v1.2.3\ngit push origin v1.2.3\n",[2423,5745,5746,5757],{"__ignoreMap":25},[2426,5747,5748,5751,5754],{"class":2428,"line":9},[2426,5749,5750],{"class":2552},"git",[2426,5752,5753],{"class":2455}," tag",[2426,5755,5756],{"class":2455}," v1.2.3\n",[2426,5758,5759,5761,5764,5767],{"class":2428,"line":20},[2426,5760,5750],{"class":2552},[2426,5762,5763],{"class":2455}," push",[2426,5765,5766],{"class":2455}," origin",[2426,5768,5756],{"class":2455},[3859,5770,5772],{"id":5771},"known-limitations","Known Limitations",[3420,5774,5775,5781,5787],{},[3423,5776,5777,5780],{},[3426,5778,5779],{},"Protected branches",": The automated release cannot bypass branch protection rules. This is by design for security.",[3423,5782,5783,5786],{},[3426,5784,5785],{},"Concurrent releases",": Rapid successive releases may fail. Simply retry after a moment.",[3423,5788,5789,5792,5793,3583,5795,5797],{},[3426,5790,5791],{},"Conventional commits required",": Version inference requires conventional commit format (",[2423,5794,5657],{},[2423,5796,5663],{},", etc.)",[3859,5799,5801],{"id":5800},"commit-conventions-for-versioning","Commit Conventions for Versioning",[3420,5803,5804,5809,5814,5820],{},[3423,5805,5806,5808],{},[2423,5807,5657],{}," new features (minor version: 1.2.0 → 1.3.0)",[3423,5810,5811,5813],{},[2423,5812,5663],{}," bug fixes (patch version: 1.2.0 → 1.2.1)",[3423,5815,5816,5819],{},[2423,5817,5818],{},"feat!:"," breaking changes (major version: 1.2.0 → 2.0.0)",[3423,5821,5822,3583,5824,3583,5826,5828],{},[2423,5823,5669],{},[2423,5825,5675],{},[2423,5827,5693],{}," no version change",[2338,5830,5831,5832],{},"Example: ",[2423,5833,5834],{},"feat(handler): add timeout support for requests",[3859,5836,5838],{"id":5837},"version-preview-on-pull-requests","Version Preview on Pull Requests",[2338,5840,5841],{},"Every PR automatically shows the next version that will be created:",[3420,5843,5844,5847,5850],{},[3423,5845,5846],{},"Check PR comments for \"Version Preview\"",[3423,5848,5849],{},"Updates automatically as you add commits",[3423,5851,5852],{},"Helps verify your commits have the intended effect",[2412,5854,5856],{"id":5855},"questions","Questions?",[3420,5858,5859,5862,5865],{},[3423,5860,5861],{},"Open an issue for questions",[3423,5863,5864],{},"Check existing issues first",[3423,5866,5867],{},"Be patient and respectful",[2338,5869,5870],{},"Thank you for contributing to rocco!",[3993,5872,5873],{},"html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}",{"title":25,"searchDepth":20,"depth":20,"links":5875},[5876,5877,5878,5883,5888,5889,5890,5891,5892,5899],{"id":5331,"depth":20,"text":5332},{"id":5338,"depth":20,"text":5339},{"id":5380,"depth":20,"text":5381,"children":5879},[5880,5881,5882],{"id":5384,"depth":31,"text":5385},{"id":5406,"depth":31,"text":1086},{"id":3849,"depth":31,"text":365},{"id":5438,"depth":20,"text":5439,"children":5884},[5885,5886,5887],{"id":5442,"depth":31,"text":5443},{"id":5460,"depth":31,"text":5461},{"id":5475,"depth":31,"text":5476},{"id":5543,"depth":20,"text":5544},{"id":5575,"depth":20,"text":1086},{"id":5634,"depth":20,"text":5635},{"id":5646,"depth":20,"text":5647},{"id":5697,"depth":20,"text":5698,"children":5893},[5894,5895,5896,5897,5898],{"id":5701,"depth":31,"text":5702},{"id":5736,"depth":31,"text":5737},{"id":5771,"depth":31,"text":5772},{"id":5800,"depth":31,"text":5801},{"id":5837,"depth":31,"text":5838},{"id":5855,"depth":20,"text":5856},{},"/contributing",{"title":3974,"description":5328},"N-1CVXDw_Ft3gxtnBFoc04D74YxTw-ASePbbrk5o45U",{"id":5905,"title":151,"author":5906,"body":5907,"description":153,"extension":4012,"meta":8594,"navigation":2478,"path":150,"published":8595,"readtime":8596,"seo":8597,"stem":2253,"tags":8598,"updated":8595,"__hash__":8600},"rocco/v0.1.21/2.learn/2.concepts.md","zoobzio",{"type":2331,"value":5908,"toc":8568},[5909,5912,5925,5927,5930,6046,6149,6151,6154,6277,6280,6283,6528,6531,6537,6654,6657,6660,6758,6761,7025,7028,7031,7203,7206,7208,7211,7305,7311,7351,7354,7357,7499,7505,7536,7539,7545,7548,7579,7582,7593,7739,7745,7748,7753,8073,8076,8082,8131,8134,8140,8176,8189,8192,8195,8270,8273,8279,8405,8410,8523,8526,8547,8550,8565],[2334,5910,151],{"id":5911},"core-concepts",[2338,5913,5914,5915,3583,5917,3583,5919,5921,5922,5924],{},"Rocco is built around a few core primitives: ",[3426,5916,160],{},[3426,5918,165],{},[3426,5920,180],{},", and ",[3426,5923,195],{},". Understanding these concepts enables you to build any HTTP API.",[2412,5926,160],{"id":4353},[2338,5928,5929],{},"The Engine is rocco's HTTP server. It manages handler registration, middleware, and request routing.",[2417,5931,5933],{"className":2419,"code":5932,"language":2421,"meta":25,"style":25},"// Create an engine\nengine := rocco.NewEngine().WithAuthenticator(extractIdentity)\n\n// Add global middleware\nengine.WithMiddleware(loggingMiddleware)\n\n// Register handlers\nengine.WithHandlers(handler1, handler2)\n\n// Start serving (HostAll = \"\", HostLocal = \"localhost\", HostLoopback = \"127.0.0.1\")\nengine.Start(rocco.HostAll, 8080)\n",[2423,5934,5935,5940,5962,5966,5971,5986,5990,5995,6015,6019,6024],{"__ignoreMap":25},[2426,5936,5937],{"class":2428,"line":9},[2426,5938,5939],{"class":2944},"// Create an engine\n",[2426,5941,5942,5944,5946,5948,5950,5952,5954,5956,5958,5960],{"class":2428,"line":20},[2426,5943,4353],{"class":2539},[2426,5945,2543],{"class":2539},[2426,5947,2546],{"class":2539},[2426,5949,2549],{"class":2442},[2426,5951,1582],{"class":2552},[2426,5953,4364],{"class":2442},[2426,5955,1591],{"class":2552},[2426,5957,2594],{"class":2442},[2426,5959,4371],{"class":2539},[2426,5961,2743],{"class":2442},[2426,5963,5964],{"class":2428,"line":31},[2426,5965,2479],{"emptyLinePlaceholder":2478},[2426,5967,5968],{"class":2428,"line":844},[2426,5969,5970],{"class":2944},"// Add global middleware\n",[2426,5972,5973,5975,5977,5979,5981,5984],{"class":2428,"line":2475},[2426,5974,4353],{"class":2539},[2426,5976,2549],{"class":2442},[2426,5978,1596],{"class":2552},[2426,5980,2594],{"class":2442},[2426,5982,5983],{"class":2539},"loggingMiddleware",[2426,5985,2743],{"class":2442},[2426,5987,5988],{"class":2428,"line":2482},[2426,5989,2479],{"emptyLinePlaceholder":2478},[2426,5991,5992],{"class":2428,"line":2494},[2426,5993,5994],{"class":2944},"// Register handlers\n",[2426,5996,5997,5999,6001,6003,6005,6008,6010,6013],{"class":2428,"line":2506},[2426,5998,4353],{"class":2539},[2426,6000,2549],{"class":2442},[2426,6002,1601],{"class":2552},[2426,6004,2594],{"class":2442},[2426,6006,6007],{"class":2539},"handler1",[2426,6009,2561],{"class":2442},[2426,6011,6012],{"class":2539}," handler2",[2426,6014,2743],{"class":2442},[2426,6016,6017],{"class":2428,"line":2516},[2426,6018,2479],{"emptyLinePlaceholder":2478},[2426,6020,6021],{"class":2428,"line":2526},[2426,6022,6023],{"class":2944},"// Start serving (HostAll = \"\", HostLocal = \"localhost\", HostLoopback = \"127.0.0.1\")\n",[2426,6025,6026,6028,6030,6032,6034,6036,6038,6040,6042,6044],{"class":2428,"line":2531},[2426,6027,4353],{"class":2539},[2426,6029,2549],{"class":2442},[2426,6031,1651],{"class":2552},[2426,6033,2594],{"class":2442},[2426,6035,2336],{"class":2539},[2426,6037,2549],{"class":2442},[2426,6039,3293],{"class":2539},[2426,6041,2561],{"class":2442},[2426,6043,3298],{"class":3199},[2426,6045,2743],{"class":2442},[3311,6047,6048,6057],{},[3314,6049,6050],{},[3317,6051,6052,6055],{},[3320,6053,6054],{},"Method",[3320,6056,3325],{},[3330,6058,6059,6069,6079,6089,6099,6109,6119,6129,6139],{},[3317,6060,6061,6066],{},[3335,6062,6063],{},[2423,6064,6065],{},"NewEngine()",[3335,6067,6068],{},"Create engine",[3317,6070,6071,6076],{},[3335,6072,6073],{},[2423,6074,6075],{},"WithAuthenticator(extractor)",[3335,6077,6078],{},"Configure identity extraction for authentication",[3317,6080,6081,6086],{},[3335,6082,6083],{},[2423,6084,6085],{},"WithMiddleware(mw...)",[3335,6087,6088],{},"Add global middleware",[3317,6090,6091,6096],{},[3335,6092,6093],{},[2423,6094,6095],{},"WithHandlers(handlers...)",[3335,6097,6098],{},"Register handlers",[3317,6100,6101,6106],{},[3335,6102,6103],{},[2423,6104,6105],{},"WithModels(models...)",[3335,6107,6108],{},"Register standalone types for OpenAPI schemas",[3317,6110,6111,6116],{},[3335,6112,6113],{},[2423,6114,6115],{},"WithSpec(spec)",[3335,6117,6118],{},"Configure OpenAPI metadata",[3317,6120,6121,6126],{},[3335,6122,6123],{},[2423,6124,6125],{},"Router()",[3335,6127,6128],{},"Access underlying stdlib ServeMux",[3317,6130,6131,6136],{},[3335,6132,6133],{},[2423,6134,6135],{},"Start(host, port)",[3335,6137,6138],{},"Begin serving requests",[3317,6140,6141,6146],{},[3335,6142,6143],{},[2423,6144,6145],{},"Shutdown(ctx)",[3335,6147,6148],{},"Graceful shutdown",[2412,6150,165],{"id":2540},[2338,6152,6153],{},"A Handler is a typed request processor. It declares input/output types and the function that processes requests.",[2417,6155,6157],{"className":2419,"code":6156,"language":2421,"meta":25,"style":25},"handler := rocco.NewHandler[InputType, OutputType](\n    \"handler-name\",  // Unique name for logging/docs\n    \"POST\",          // HTTP method\n    \"/path\",         // URL path\n    func(req *rocco.Request[InputType]) (OutputType, error) {\n        // Process request, return response or error\n        return OutputType{...}, nil\n    },\n)\n",[2423,6158,6159,6183,6193,6203,6213,6248,6253,6269,6273],{"__ignoreMap":25},[2426,6160,6161,6163,6165,6167,6169,6171,6173,6176,6178,6181],{"class":2428,"line":9},[2426,6162,2540],{"class":2539},[2426,6164,2543],{"class":2539},[2426,6166,2546],{"class":2539},[2426,6168,2549],{"class":2442},[2426,6170,1669],{"class":2552},[2426,6172,2555],{"class":2442},[2426,6174,6175],{"class":2435},"InputType",[2426,6177,2561],{"class":2442},[2426,6179,6180],{"class":2435}," OutputType",[2426,6182,2566],{"class":2442},[2426,6184,6185,6188,6190],{"class":2428,"line":20},[2426,6186,6187],{"class":2455},"    \"handler-name\"",[2426,6189,2561],{"class":2442},[2426,6191,6192],{"class":2944},"  // Unique name for logging/docs\n",[2426,6194,6195,6198,6200],{"class":2428,"line":31},[2426,6196,6197],{"class":2455},"    \"POST\"",[2426,6199,2561],{"class":2442},[2426,6201,6202],{"class":2944},"          // HTTP method\n",[2426,6204,6205,6208,6210],{"class":2428,"line":844},[2426,6206,6207],{"class":2455},"    \"/path\"",[2426,6209,2561],{"class":2442},[2426,6211,6212],{"class":2944},"         // URL path\n",[2426,6214,6215,6217,6219,6221,6223,6225,6227,6229,6231,6233,6235,6237,6240,6242,6244,6246],{"class":2428,"line":2475},[2426,6216,2591],{"class":2431},[2426,6218,2594],{"class":2442},[2426,6220,2598],{"class":2597},[2426,6222,2602],{"class":2601},[2426,6224,2336],{"class":2435},[2426,6226,2549],{"class":2442},[2426,6228,180],{"class":2435},[2426,6230,2555],{"class":2442},[2426,6232,6175],{"class":2435},[2426,6234,2615],{"class":2442},[2426,6236,2618],{"class":2442},[2426,6238,6239],{"class":2435},"OutputType",[2426,6241,2561],{"class":2442},[2426,6243,2626],{"class":2435},[2426,6245,2629],{"class":2442},[2426,6247,2443],{"class":2442},[2426,6249,6250],{"class":2428,"line":2482},[2426,6251,6252],{"class":2944},"        // Process request, return response or error\n",[2426,6254,6255,6257,6259,6261,6264,6267],{"class":2428,"line":2494},[2426,6256,2637],{"class":2601},[2426,6258,6180],{"class":2435},[2426,6260,4458],{"class":2442},[2426,6262,6263],{"class":2601},"...",[2426,6265,6266],{"class":2442},"},",[2426,6268,2710],{"class":2431},[2426,6270,6271],{"class":2428,"line":2506},[2426,6272,2716],{"class":2442},[2426,6274,6275],{"class":2428,"line":2516},[2426,6276,2743],{"class":2442},[3859,6278,170],{"id":6279},"handler-configuration",[2338,6281,6282],{},"Handlers use a builder pattern for configuration:",[2417,6284,6286],{"className":2419,"code":6285,"language":2421,"meta":25,"style":25},"handler := rocco.NewHandler[CreateOrderInput, OrderOutput](\n    \"create-order\",\n    \"POST\",\n    \"/orders\",\n    handleCreateOrder,\n).\n    WithSummary(\"Create a new order\").              // OpenAPI summary\n    WithDescription(\"Creates an order...\").         // OpenAPI description\n    WithTags(\"orders\").                             // OpenAPI tags\n    WithSuccessStatus(201).                         // Success status code\n    WithPathParams(\"id\").                           // Declare path params\n    WithQueryParams(\"include\").                     // Declare query params\n    WithErrors(ErrNotFound, ErrConflict).           // Declare possible errors\n    WithAuthentication().                           // Require authentication\n    WithScopes(\"orders:write\").                     // Require scope\n    WithRoles(\"admin\", \"manager\").                  // Require role\n    WithMaxBodySize(1024 * 1024).                   // 1MB body limit\n    WithMiddleware(customMiddleware)                // Handler-specific middleware\n",[2423,6287,6288,6311,6318,6324,6331,6338,6342,6357,6372,6387,6401,6415,6430,6449,6459,6474,6494,6513],{"__ignoreMap":25},[2426,6289,6290,6292,6294,6296,6298,6300,6302,6304,6306,6309],{"class":2428,"line":9},[2426,6291,2540],{"class":2539},[2426,6293,2543],{"class":2539},[2426,6295,2546],{"class":2539},[2426,6297,2549],{"class":2442},[2426,6299,1669],{"class":2552},[2426,6301,2555],{"class":2442},[2426,6303,3555],{"class":2435},[2426,6305,2561],{"class":2442},[2426,6307,6308],{"class":2435}," OrderOutput",[2426,6310,2566],{"class":2442},[2426,6312,6313,6316],{"class":2428,"line":20},[2426,6314,6315],{"class":2455},"    \"create-order\"",[2426,6317,2585],{"class":2442},[2426,6319,6320,6322],{"class":2428,"line":31},[2426,6321,6197],{"class":2455},[2426,6323,2585],{"class":2442},[2426,6325,6326,6329],{"class":2428,"line":844},[2426,6327,6328],{"class":2455},"    \"/orders\"",[2426,6330,2585],{"class":2442},[2426,6332,6333,6336],{"class":2428,"line":2475},[2426,6334,6335],{"class":2539},"    handleCreateOrder",[2426,6337,2585],{"class":2442},[2426,6339,6340],{"class":2428,"line":2482},[2426,6341,3175],{"class":2442},[2426,6343,6344,6347,6349,6352,6354],{"class":2428,"line":2494},[2426,6345,6346],{"class":2552},"    WithSummary",[2426,6348,2594],{"class":2442},[2426,6350,6351],{"class":2455},"\"Create a new order\"",[2426,6353,2722],{"class":2442},[2426,6355,6356],{"class":2944},"              // OpenAPI summary\n",[2426,6358,6359,6362,6364,6367,6369],{"class":2428,"line":2506},[2426,6360,6361],{"class":2552},"    WithDescription",[2426,6363,2594],{"class":2442},[2426,6365,6366],{"class":2455},"\"Creates an order...\"",[2426,6368,2722],{"class":2442},[2426,6370,6371],{"class":2944},"         // OpenAPI description\n",[2426,6373,6374,6377,6379,6382,6384],{"class":2428,"line":2516},[2426,6375,6376],{"class":2552},"    WithTags",[2426,6378,2594],{"class":2442},[2426,6380,6381],{"class":2455},"\"orders\"",[2426,6383,2722],{"class":2442},[2426,6385,6386],{"class":2944},"                             // OpenAPI tags\n",[2426,6388,6389,6392,6394,6396,6398],{"class":2428,"line":2526},[2426,6390,6391],{"class":2552},"    WithSuccessStatus",[2426,6393,2594],{"class":2442},[2426,6395,3200],{"class":3199},[2426,6397,2722],{"class":2442},[2426,6399,6400],{"class":2944},"                         // Success status code\n",[2426,6402,6403,6406,6408,6410,6412],{"class":2428,"line":2531},[2426,6404,6405],{"class":2552},"    WithPathParams",[2426,6407,2594],{"class":2442},[2426,6409,5134],{"class":2455},[2426,6411,2722],{"class":2442},[2426,6413,6414],{"class":2944},"                           // Declare path params\n",[2426,6416,6417,6420,6422,6425,6427],{"class":2428,"line":2536},[2426,6418,6419],{"class":2552},"    WithQueryParams",[2426,6421,2594],{"class":2442},[2426,6423,6424],{"class":2455},"\"include\"",[2426,6426,2722],{"class":2442},[2426,6428,6429],{"class":2944},"                     // Declare query params\n",[2426,6431,6432,6435,6437,6439,6441,6444,6446],{"class":2428,"line":2569},[2426,6433,6434],{"class":2552},"    WithErrors",[2426,6436,2594],{"class":2442},[2426,6438,1934],{"class":2539},[2426,6440,2561],{"class":2442},[2426,6442,6443],{"class":2539}," ErrConflict",[2426,6445,2722],{"class":2442},[2426,6447,6448],{"class":2944},"           // Declare possible errors\n",[2426,6450,6451,6454,6456],{"class":2428,"line":2588},[2426,6452,6453],{"class":2552},"    WithAuthentication",[2426,6455,4364],{"class":2442},[2426,6457,6458],{"class":2944},"                           // Require authentication\n",[2426,6460,6461,6464,6466,6469,6471],{"class":2428,"line":2634},[2426,6462,6463],{"class":2552},"    WithScopes",[2426,6465,2594],{"class":2442},[2426,6467,6468],{"class":2455},"\"orders:write\"",[2426,6470,2722],{"class":2442},[2426,6472,6473],{"class":2944},"                     // Require scope\n",[2426,6475,6476,6479,6481,6484,6486,6489,6491],{"class":2428,"line":2645},[2426,6477,6478],{"class":2552},"    WithRoles",[2426,6480,2594],{"class":2442},[2426,6482,6483],{"class":2455},"\"admin\"",[2426,6485,2561],{"class":2442},[2426,6487,6488],{"class":2455}," \"manager\"",[2426,6490,2722],{"class":2442},[2426,6492,6493],{"class":2944},"                  // Require role\n",[2426,6495,6496,6499,6501,6504,6506,6508,6510],{"class":2428,"line":2659},[2426,6497,6498],{"class":2552},"    WithMaxBodySize",[2426,6500,2594],{"class":2442},[2426,6502,6503],{"class":3199},"1024",[2426,6505,2602],{"class":2539},[2426,6507,4926],{"class":3199},[2426,6509,2722],{"class":2442},[2426,6511,6512],{"class":2944},"                   // 1MB body limit\n",[2426,6514,6515,6518,6520,6523,6525],{"class":2428,"line":2682},[2426,6516,6517],{"class":2552},"    WithMiddleware",[2426,6519,2594],{"class":2442},[2426,6521,6522],{"class":2539},"customMiddleware",[2426,6524,2629],{"class":2442},[2426,6526,6527],{"class":2944},"                // Handler-specific middleware\n",[3859,6529,175],{"id":6530},"nobody-type",[2338,6532,6533,6534,2651],{},"For handlers without request bodies (GET, DELETE), use ",[2423,6535,6536],{},"rocco.NoBody",[2417,6538,6540],{"className":2419,"code":6539,"language":2421,"meta":25,"style":25},"handler := rocco.NewHandler[rocco.NoBody, UserOutput](\n    \"get-user\",\n    \"GET\",\n    \"/users/{id}\",\n    func(req *rocco.Request[rocco.NoBody]) (UserOutput, error) {\n        // req.Body is NoBody (empty struct)\n        return UserOutput{...}, nil\n    },\n)\n",[2423,6541,6542,6568,6575,6582,6589,6627,6632,6646,6650],{"__ignoreMap":25},[2426,6543,6544,6546,6548,6550,6552,6554,6556,6558,6560,6562,6564,6566],{"class":2428,"line":9},[2426,6545,2540],{"class":2539},[2426,6547,2543],{"class":2539},[2426,6549,2546],{"class":2539},[2426,6551,2549],{"class":2442},[2426,6553,1669],{"class":2552},[2426,6555,2555],{"class":2442},[2426,6557,2336],{"class":2435},[2426,6559,2549],{"class":2442},[2426,6561,1809],{"class":2435},[2426,6563,2561],{"class":2442},[2426,6565,2487],{"class":2435},[2426,6567,2566],{"class":2442},[2426,6569,6570,6573],{"class":2428,"line":20},[2426,6571,6572],{"class":2455},"    \"get-user\"",[2426,6574,2585],{"class":2442},[2426,6576,6577,6580],{"class":2428,"line":31},[2426,6578,6579],{"class":2455},"    \"GET\"",[2426,6581,2585],{"class":2442},[2426,6583,6584,6587],{"class":2428,"line":844},[2426,6585,6586],{"class":2455},"    \"/users/{id}\"",[2426,6588,2585],{"class":2442},[2426,6590,6591,6593,6595,6597,6599,6601,6603,6605,6607,6609,6611,6613,6615,6617,6619,6621,6623,6625],{"class":2428,"line":2475},[2426,6592,2591],{"class":2431},[2426,6594,2594],{"class":2442},[2426,6596,2598],{"class":2597},[2426,6598,2602],{"class":2601},[2426,6600,2336],{"class":2435},[2426,6602,2549],{"class":2442},[2426,6604,180],{"class":2435},[2426,6606,2555],{"class":2442},[2426,6608,2336],{"class":2435},[2426,6610,2549],{"class":2442},[2426,6612,1809],{"class":2435},[2426,6614,2615],{"class":2442},[2426,6616,2618],{"class":2442},[2426,6618,2621],{"class":2435},[2426,6620,2561],{"class":2442},[2426,6622,2626],{"class":2435},[2426,6624,2629],{"class":2442},[2426,6626,2443],{"class":2442},[2426,6628,6629],{"class":2428,"line":2482},[2426,6630,6631],{"class":2944},"        // req.Body is NoBody (empty struct)\n",[2426,6633,6634,6636,6638,6640,6642,6644],{"class":2428,"line":2494},[2426,6635,2637],{"class":2601},[2426,6637,2487],{"class":2435},[2426,6639,4458],{"class":2442},[2426,6641,6263],{"class":2601},[2426,6643,6266],{"class":2442},[2426,6645,2710],{"class":2431},[2426,6647,6648],{"class":2428,"line":2506},[2426,6649,2716],{"class":2442},[2426,6651,6652],{"class":2428,"line":2516},[2426,6653,2743],{"class":2442},[2412,6655,180],{"id":6656},"request",[2338,6658,6659],{},"The Request struct provides access to the parsed body, parameters, identity, and underlying HTTP request.",[2417,6661,6663],{"className":2419,"code":6662,"language":2421,"meta":25,"style":25},"type Request[T any] struct {\n    Context  context.Context    // Request context\n    Request  *http.Request      // Underlying HTTP request\n    Params   *Params            // Path and query parameters\n    Body     T                  // Parsed and validated body\n    Identity Identity           // Authenticated identity (if any)\n}\n",[2423,6664,6665,6687,6702,6719,6732,6743,6754],{"__ignoreMap":25},[2426,6666,6667,6669,6672,6674,6677,6680,6683,6685],{"class":2428,"line":9},[2426,6668,2432],{"class":2431},[2426,6670,6671],{"class":2435}," Request",[2426,6673,2555],{"class":2442},[2426,6675,6676],{"class":2597},"T",[2426,6678,6679],{"class":2435}," any",[2426,6681,6682],{"class":2442},"]",[2426,6684,2439],{"class":2431},[2426,6686,2443],{"class":2442},[2426,6688,6689,6692,6695,6697,6699],{"class":2428,"line":20},[2426,6690,6691],{"class":2448},"    Context",[2426,6693,6694],{"class":2435},"  context",[2426,6696,2549],{"class":2442},[2426,6698,4609],{"class":2435},[2426,6700,6701],{"class":2944},"    // Request context\n",[2426,6703,6704,6707,6710,6712,6714,6716],{"class":2428,"line":31},[2426,6705,6706],{"class":2448},"    Request",[2426,6708,6709],{"class":2601},"  *",[2426,6711,4619],{"class":2435},[2426,6713,2549],{"class":2442},[2426,6715,180],{"class":2435},[2426,6717,6718],{"class":2944},"      // Underlying HTTP request\n",[2426,6720,6721,6724,6727,6729],{"class":2428,"line":844},[2426,6722,6723],{"class":2448},"    Params",[2426,6725,6726],{"class":2601},"   *",[2426,6728,1804],{"class":2435},[2426,6730,6731],{"class":2944},"            // Path and query parameters\n",[2426,6733,6734,6737,6740],{"class":2428,"line":2475},[2426,6735,6736],{"class":2448},"    Body",[2426,6738,6739],{"class":2435},"     T",[2426,6741,6742],{"class":2944},"                  // Parsed and validated body\n",[2426,6744,6745,6748,6751],{"class":2428,"line":2482},[2426,6746,6747],{"class":2448},"    Identity",[2426,6749,6750],{"class":2435}," Identity",[2426,6752,6753],{"class":2944},"           // Authenticated identity (if any)\n",[2426,6755,6756],{"class":2428,"line":2494},[2426,6757,2472],{"class":2442},[3859,6759,185],{"id":6760},"accessing-parameters",[2417,6762,6764],{"className":2419,"code":6763,"language":2421,"meta":25,"style":25},"func(req *rocco.Request[MyInput]) (MyOutput, error) {\n    // Path parameters\n    userID := req.Params.Path[\"id\"]\n\n    // Query parameters\n    page := req.Params.Query[\"page\"]\n    filter := req.Params.Query[\"filter\"]\n\n    // Request body (already parsed and validated)\n    name := req.Body.Name\n\n    // Identity (if authenticated)\n    if req.Identity != nil {\n        tenantID := req.Identity.TenantID()\n    }\n\n    // Underlying request\n    header := req.Request.Header.Get(\"X-Custom\")\n\n    return MyOutput{...}, nil\n}\n",[2423,6765,6766,6804,6809,6832,6836,6841,6865,6889,6893,6898,6916,6920,6925,6941,6961,6965,6969,6974,7002,7006,7021],{"__ignoreMap":25},[2426,6767,6768,6770,6772,6775,6778,6780,6782,6784,6786,6789,6791,6793,6796,6798,6800,6802],{"class":2428,"line":9},[2426,6769,2908],{"class":2431},[2426,6771,2594],{"class":2442},[2426,6773,6774],{"class":2597},"req ",[2426,6776,6777],{"class":2601},"*",[2426,6779,2336],{"class":2435},[2426,6781,2549],{"class":2442},[2426,6783,180],{"class":2435},[2426,6785,2555],{"class":2442},[2426,6787,6788],{"class":2435},"MyInput",[2426,6790,2615],{"class":2442},[2426,6792,2618],{"class":2442},[2426,6794,6795],{"class":2435},"MyOutput",[2426,6797,2561],{"class":2442},[2426,6799,2626],{"class":2435},[2426,6801,2629],{"class":2442},[2426,6803,2443],{"class":2442},[2426,6805,6806],{"class":2428,"line":20},[2426,6807,6808],{"class":2944},"    // Path parameters\n",[2426,6810,6811,6814,6816,6818,6820,6822,6824,6826,6828,6830],{"class":2428,"line":31},[2426,6812,6813],{"class":2539},"    userID",[2426,6815,2543],{"class":2539},[2426,6817,2690],{"class":2539},[2426,6819,2549],{"class":2442},[2426,6821,1804],{"class":2539},[2426,6823,2549],{"class":2442},[2426,6825,5129],{"class":2539},[2426,6827,2555],{"class":2442},[2426,6829,5134],{"class":2455},[2426,6831,3594],{"class":2442},[2426,6833,6834],{"class":2428,"line":844},[2426,6835,2479],{"emptyLinePlaceholder":2478},[2426,6837,6838],{"class":2428,"line":2475},[2426,6839,6840],{"class":2944},"    // Query parameters\n",[2426,6842,6843,6846,6848,6850,6852,6854,6856,6858,6860,6863],{"class":2428,"line":2482},[2426,6844,6845],{"class":2539},"    page",[2426,6847,2543],{"class":2539},[2426,6849,2690],{"class":2539},[2426,6851,2549],{"class":2442},[2426,6853,1804],{"class":2539},[2426,6855,2549],{"class":2442},[2426,6857,5110],{"class":2539},[2426,6859,2555],{"class":2442},[2426,6861,6862],{"class":2455},"\"page\"",[2426,6864,3594],{"class":2442},[2426,6866,6867,6870,6872,6874,6876,6878,6880,6882,6884,6887],{"class":2428,"line":2494},[2426,6868,6869],{"class":2539},"    filter",[2426,6871,2543],{"class":2539},[2426,6873,2690],{"class":2539},[2426,6875,2549],{"class":2442},[2426,6877,1804],{"class":2539},[2426,6879,2549],{"class":2442},[2426,6881,5110],{"class":2539},[2426,6883,2555],{"class":2442},[2426,6885,6886],{"class":2455},"\"filter\"",[2426,6888,3594],{"class":2442},[2426,6890,6891],{"class":2428,"line":2506},[2426,6892,2479],{"emptyLinePlaceholder":2478},[2426,6894,6895],{"class":2428,"line":2516},[2426,6896,6897],{"class":2944},"    // Request body (already parsed and validated)\n",[2426,6899,6900,6903,6905,6907,6909,6911,6913],{"class":2428,"line":2526},[2426,6901,6902],{"class":2539},"    name",[2426,6904,2543],{"class":2539},[2426,6906,2690],{"class":2539},[2426,6908,2549],{"class":2442},[2426,6910,2672],{"class":2539},[2426,6912,2549],{"class":2442},[2426,6914,6915],{"class":2539},"Name\n",[2426,6917,6918],{"class":2428,"line":2531},[2426,6919,2479],{"emptyLinePlaceholder":2478},[2426,6921,6922],{"class":2428,"line":2536},[2426,6923,6924],{"class":2944},"    // Identity (if authenticated)\n",[2426,6926,6927,6929,6931,6933,6935,6937,6939],{"class":2428,"line":2569},[2426,6928,4677],{"class":2601},[2426,6930,2690],{"class":2539},[2426,6932,2549],{"class":2442},[2426,6934,245],{"class":2539},[2426,6936,4773],{"class":2601},[2426,6938,4695],{"class":2431},[2426,6940,2443],{"class":2442},[2426,6942,6943,6946,6948,6950,6952,6954,6956,6959],{"class":2428,"line":2588},[2426,6944,6945],{"class":2539},"        tenantID",[2426,6947,2543],{"class":2539},[2426,6949,2690],{"class":2539},[2426,6951,2549],{"class":2442},[2426,6953,245],{"class":2539},[2426,6955,2549],{"class":2442},[2426,6957,6958],{"class":2552},"TenantID",[2426,6960,2933],{"class":2442},[2426,6962,6963],{"class":2428,"line":2634},[2426,6964,4717],{"class":2442},[2426,6966,6967],{"class":2428,"line":2645},[2426,6968,2479],{"emptyLinePlaceholder":2478},[2426,6970,6971],{"class":2428,"line":2659},[2426,6972,6973],{"class":2944},"    // Underlying request\n",[2426,6975,6976,6979,6981,6983,6985,6987,6989,6991,6993,6995,6997,7000],{"class":2428,"line":2682},[2426,6977,6978],{"class":2539},"    header",[2426,6980,2543],{"class":2539},[2426,6982,2690],{"class":2539},[2426,6984,2549],{"class":2442},[2426,6986,180],{"class":2539},[2426,6988,2549],{"class":2442},[2426,6990,4660],{"class":2539},[2426,6992,2549],{"class":2442},[2426,6994,4665],{"class":2552},[2426,6996,2594],{"class":2442},[2426,6998,6999],{"class":2455},"\"X-Custom\"",[2426,7001,2743],{"class":2442},[2426,7003,7004],{"class":2428,"line":2704},[2426,7005,2479],{"emptyLinePlaceholder":2478},[2426,7007,7008,7010,7013,7015,7017,7019],{"class":2428,"line":2713},[2426,7009,4808],{"class":2601},[2426,7011,7012],{"class":2435}," MyOutput",[2426,7014,4458],{"class":2442},[2426,7016,6263],{"class":2601},[2426,7018,6266],{"class":2442},[2426,7020,2710],{"class":2431},[2426,7022,7023],{"class":2428,"line":2719},[2426,7024,2472],{"class":2442},[3859,7026,190],{"id":7027},"parameter-declaration",[2338,7029,7030],{},"Handlers must declare their parameters:",[2417,7032,7034],{"className":2419,"code":7033,"language":2421,"meta":25,"style":25},"// Path parameters use {param} syntax\nhandler := rocco.NewHandler[rocco.NoBody, Output](\n    \"get-item\",\n    \"GET\",\n    \"/items/{category}/{id}\",\n    handleGetItem,\n).WithPathParams(\"category\", \"id\")\n\n// Query parameters\nhandler := rocco.NewHandler[rocco.NoBody, Output](\n    \"search\",\n    \"GET\",\n    \"/search\",\n    handleSearch,\n).WithQueryParams(\"q\", \"page\", \"limit\", \"sort\")\n",[2423,7035,7036,7041,7068,7075,7081,7088,7095,7113,7117,7122,7148,7155,7161,7168,7175],{"__ignoreMap":25},[2426,7037,7038],{"class":2428,"line":9},[2426,7039,7040],{"class":2944},"// Path parameters use {param} syntax\n",[2426,7042,7043,7045,7047,7049,7051,7053,7055,7057,7059,7061,7063,7066],{"class":2428,"line":20},[2426,7044,2540],{"class":2539},[2426,7046,2543],{"class":2539},[2426,7048,2546],{"class":2539},[2426,7050,2549],{"class":2442},[2426,7052,1669],{"class":2552},[2426,7054,2555],{"class":2442},[2426,7056,2336],{"class":2435},[2426,7058,2549],{"class":2442},[2426,7060,1809],{"class":2435},[2426,7062,2561],{"class":2442},[2426,7064,7065],{"class":2435}," Output",[2426,7067,2566],{"class":2442},[2426,7069,7070,7073],{"class":2428,"line":31},[2426,7071,7072],{"class":2455},"    \"get-item\"",[2426,7074,2585],{"class":2442},[2426,7076,7077,7079],{"class":2428,"line":844},[2426,7078,6579],{"class":2455},[2426,7080,2585],{"class":2442},[2426,7082,7083,7086],{"class":2428,"line":2475},[2426,7084,7085],{"class":2455},"    \"/items/{category}/{id}\"",[2426,7087,2585],{"class":2442},[2426,7089,7090,7093],{"class":2428,"line":2482},[2426,7091,7092],{"class":2539},"    handleGetItem",[2426,7094,2585],{"class":2442},[2426,7096,7097,7099,7101,7103,7106,7108,7111],{"class":2428,"line":2494},[2426,7098,2722],{"class":2442},[2426,7100,1703],{"class":2552},[2426,7102,2594],{"class":2442},[2426,7104,7105],{"class":2455},"\"category\"",[2426,7107,2561],{"class":2442},[2426,7109,7110],{"class":2455}," \"id\"",[2426,7112,2743],{"class":2442},[2426,7114,7115],{"class":2428,"line":2506},[2426,7116,2479],{"emptyLinePlaceholder":2478},[2426,7118,7119],{"class":2428,"line":2516},[2426,7120,7121],{"class":2944},"// Query parameters\n",[2426,7123,7124,7126,7128,7130,7132,7134,7136,7138,7140,7142,7144,7146],{"class":2428,"line":2526},[2426,7125,2540],{"class":2539},[2426,7127,2543],{"class":2539},[2426,7129,2546],{"class":2539},[2426,7131,2549],{"class":2442},[2426,7133,1669],{"class":2552},[2426,7135,2555],{"class":2442},[2426,7137,2336],{"class":2435},[2426,7139,2549],{"class":2442},[2426,7141,1809],{"class":2435},[2426,7143,2561],{"class":2442},[2426,7145,7065],{"class":2435},[2426,7147,2566],{"class":2442},[2426,7149,7150,7153],{"class":2428,"line":2531},[2426,7151,7152],{"class":2455},"    \"search\"",[2426,7154,2585],{"class":2442},[2426,7156,7157,7159],{"class":2428,"line":2536},[2426,7158,6579],{"class":2455},[2426,7160,2585],{"class":2442},[2426,7162,7163,7166],{"class":2428,"line":2569},[2426,7164,7165],{"class":2455},"    \"/search\"",[2426,7167,2585],{"class":2442},[2426,7169,7170,7173],{"class":2428,"line":2588},[2426,7171,7172],{"class":2539},"    handleSearch",[2426,7174,2585],{"class":2442},[2426,7176,7177,7179,7181,7183,7186,7188,7191,7193,7196,7198,7201],{"class":2428,"line":2634},[2426,7178,2722],{"class":2442},[2426,7180,1708],{"class":2552},[2426,7182,2594],{"class":2442},[2426,7184,7185],{"class":2455},"\"q\"",[2426,7187,2561],{"class":2442},[2426,7189,7190],{"class":2455}," \"page\"",[2426,7192,2561],{"class":2442},[2426,7194,7195],{"class":2455}," \"limit\"",[2426,7197,2561],{"class":2442},[2426,7199,7200],{"class":2455}," \"sort\"",[2426,7202,2743],{"class":2442},[2412,7204,195],{"id":7205},"response",[2338,7207,197],{},[3859,7209,200],{"id":7210},"success-responses",[2417,7212,7214],{"className":2419,"code":7213,"language":2421,"meta":25,"style":25},"func(req *rocco.Request[Input]) (Output, error) {\n    // Return output - automatically JSON-encoded\n    return Output{\n        ID:   \"123\",\n        Name: req.Body.Name,\n    }, nil\n}\n",[2423,7215,7216,7252,7257,7265,7276,7295,7301],{"__ignoreMap":25},[2426,7217,7218,7220,7222,7224,7226,7228,7230,7232,7234,7237,7239,7241,7244,7246,7248,7250],{"class":2428,"line":9},[2426,7219,2908],{"class":2431},[2426,7221,2594],{"class":2442},[2426,7223,6774],{"class":2597},[2426,7225,6777],{"class":2601},[2426,7227,2336],{"class":2435},[2426,7229,2549],{"class":2442},[2426,7231,180],{"class":2435},[2426,7233,2555],{"class":2442},[2426,7235,7236],{"class":2435},"Input",[2426,7238,2615],{"class":2442},[2426,7240,2618],{"class":2442},[2426,7242,7243],{"class":2435},"Output",[2426,7245,2561],{"class":2442},[2426,7247,2626],{"class":2435},[2426,7249,2629],{"class":2442},[2426,7251,2443],{"class":2442},[2426,7253,7254],{"class":2428,"line":20},[2426,7255,7256],{"class":2944},"    // Return output - automatically JSON-encoded\n",[2426,7258,7259,7261,7263],{"class":2428,"line":31},[2426,7260,4808],{"class":2601},[2426,7262,7065],{"class":2435},[2426,7264,2642],{"class":2442},[2426,7266,7267,7269,7271,7274],{"class":2428,"line":844},[2426,7268,4821],{"class":2448},[2426,7270,2651],{"class":2442},[2426,7272,7273],{"class":2455},"   \"123\"",[2426,7275,2585],{"class":2442},[2426,7277,7278,7281,7283,7285,7287,7289,7291,7293],{"class":2428,"line":2475},[2426,7279,7280],{"class":2448},"        Name",[2426,7282,2651],{"class":2442},[2426,7284,2690],{"class":2539},[2426,7286,2549],{"class":2442},[2426,7288,2672],{"class":2539},[2426,7290,2549],{"class":2442},[2426,7292,2677],{"class":2539},[2426,7294,2585],{"class":2442},[2426,7296,7297,7299],{"class":2428,"line":2482},[2426,7298,4872],{"class":2442},[2426,7300,2710],{"class":2431},[2426,7302,7303],{"class":2428,"line":2494},[2426,7304,2472],{"class":2442},[2338,7306,7307,7308,2651],{},"Default success status is 200 OK. Override with ",[2423,7309,7310],{},"WithSuccessStatus()",[2417,7312,7314],{"className":2419,"code":7313,"language":2421,"meta":25,"style":25},"handler.WithSuccessStatus(201) // 201 Created\nhandler.WithSuccessStatus(204) // 204 No Content\n",[2423,7315,7316,7333],{"__ignoreMap":25},[2426,7317,7318,7320,7322,7324,7326,7328,7330],{"class":2428,"line":9},[2426,7319,2540],{"class":2539},[2426,7321,2549],{"class":2442},[2426,7323,1698],{"class":2552},[2426,7325,2594],{"class":2442},[2426,7327,3200],{"class":3199},[2426,7329,2629],{"class":2442},[2426,7331,7332],{"class":2944}," // 201 Created\n",[2426,7334,7335,7337,7339,7341,7343,7346,7348],{"class":2428,"line":20},[2426,7336,2540],{"class":2539},[2426,7338,2549],{"class":2442},[2426,7340,1698],{"class":2552},[2426,7342,2594],{"class":2442},[2426,7344,7345],{"class":3199},"204",[2426,7347,2629],{"class":2442},[2426,7349,7350],{"class":2944}," // 204 No Content\n",[3859,7352,205],{"id":7353},"error-responses",[2338,7355,7356],{},"Return errors to send error responses:",[2417,7358,7360],{"className":2419,"code":7359,"language":2421,"meta":25,"style":25},"func(req *rocco.Request[Input]) (Output, error) {\n    user, err := db.FindUser(req.Params.Path[\"id\"])\n    if err != nil {\n        // Sentinel error - returns structured JSON response\n        return Output{}, rocco.ErrNotFound.WithMessage(\"user not found\")\n    }\n    return Output{...}, nil\n}\n",[2423,7361,7362,7396,7433,7445,7450,7477,7481,7495],{"__ignoreMap":25},[2426,7363,7364,7366,7368,7370,7372,7374,7376,7378,7380,7382,7384,7386,7388,7390,7392,7394],{"class":2428,"line":9},[2426,7365,2908],{"class":2431},[2426,7367,2594],{"class":2442},[2426,7369,6774],{"class":2597},[2426,7371,6777],{"class":2601},[2426,7373,2336],{"class":2435},[2426,7375,2549],{"class":2442},[2426,7377,180],{"class":2435},[2426,7379,2555],{"class":2442},[2426,7381,7236],{"class":2435},[2426,7383,2615],{"class":2442},[2426,7385,2618],{"class":2442},[2426,7387,7243],{"class":2435},[2426,7389,2561],{"class":2442},[2426,7391,2626],{"class":2435},[2426,7393,2629],{"class":2442},[2426,7395,2443],{"class":2442},[2426,7397,7398,7401,7403,7405,7407,7410,7412,7415,7417,7419,7421,7423,7425,7427,7429,7431],{"class":2428,"line":20},[2426,7399,7400],{"class":2539},"    user",[2426,7402,2561],{"class":2442},[2426,7404,4736],{"class":2539},[2426,7406,2543],{"class":2539},[2426,7408,7409],{"class":2539}," db",[2426,7411,2549],{"class":2442},[2426,7413,7414],{"class":2552},"FindUser",[2426,7416,2594],{"class":2442},[2426,7418,2598],{"class":2539},[2426,7420,2549],{"class":2442},[2426,7422,1804],{"class":2539},[2426,7424,2549],{"class":2442},[2426,7426,5129],{"class":2539},[2426,7428,2555],{"class":2442},[2426,7430,5134],{"class":2455},[2426,7432,5137],{"class":2442},[2426,7434,7435,7437,7439,7441,7443],{"class":2428,"line":31},[2426,7436,4677],{"class":2601},[2426,7438,4736],{"class":2539},[2426,7440,4773],{"class":2601},[2426,7442,4695],{"class":2431},[2426,7444,2443],{"class":2442},[2426,7446,7447],{"class":2428,"line":844},[2426,7448,7449],{"class":2944},"        // Sentinel error - returns structured JSON response\n",[2426,7451,7452,7454,7456,7459,7461,7463,7465,7467,7470,7472,7475],{"class":2428,"line":2475},[2426,7453,2637],{"class":2601},[2426,7455,7065],{"class":2435},[2426,7457,7458],{"class":2442},"{},",[2426,7460,2546],{"class":2539},[2426,7462,2549],{"class":2442},[2426,7464,1934],{"class":2539},[2426,7466,2549],{"class":2442},[2426,7468,7469],{"class":2552},"WithMessage",[2426,7471,2594],{"class":2442},[2426,7473,7474],{"class":2455},"\"user not found\"",[2426,7476,2743],{"class":2442},[2426,7478,7479],{"class":2428,"line":2482},[2426,7480,4717],{"class":2442},[2426,7482,7483,7485,7487,7489,7491,7493],{"class":2428,"line":2494},[2426,7484,4808],{"class":2601},[2426,7486,7065],{"class":2435},[2426,7488,4458],{"class":2442},[2426,7490,6263],{"class":2601},[2426,7492,6266],{"class":2442},[2426,7494,2710],{"class":2431},[2426,7496,7497],{"class":2428,"line":2506},[2426,7498,2472],{"class":2442},[2338,7500,7501,7502,2651],{},"Errors must be declared with ",[2423,7503,7504],{},"WithErrors()",[2417,7506,7508],{"className":2419,"code":7507,"language":2421,"meta":25,"style":25},"handler.WithErrors(rocco.ErrNotFound, rocco.ErrConflict)\n",[2423,7509,7510],{"__ignoreMap":25},[2426,7511,7512,7514,7516,7518,7520,7522,7524,7526,7528,7530,7532,7534],{"class":2428,"line":9},[2426,7513,2540],{"class":2539},[2426,7515,2549],{"class":2442},[2426,7517,1718],{"class":2552},[2426,7519,2594],{"class":2442},[2426,7521,2336],{"class":2539},[2426,7523,2549],{"class":2442},[2426,7525,1934],{"class":2539},[2426,7527,2561],{"class":2442},[2426,7529,2546],{"class":2539},[2426,7531,2549],{"class":2442},[2426,7533,1939],{"class":2539},[2426,7535,2743],{"class":2442},[2412,7537,210],{"id":7538},"validation",[2338,7540,7541,7542,7544],{},"Validation is opt-in via the ",[2423,7543,1862],{}," interface. Types that implement this interface are automatically validated.",[3859,7546,215],{"id":7547},"validatable-interface",[2417,7549,7551],{"className":2419,"code":7550,"language":2421,"meta":25,"style":25},"type Validatable interface {\n    Validate() error\n}\n",[2423,7552,7553,7565,7575],{"__ignoreMap":25},[2426,7554,7555,7557,7560,7563],{"class":2428,"line":9},[2426,7556,2432],{"class":2431},[2426,7558,7559],{"class":2435}," Validatable",[2426,7561,7562],{"class":2431}," interface",[2426,7564,2443],{"class":2442},[2426,7566,7567,7570,7572],{"class":2428,"line":20},[2426,7568,7569],{"class":2552},"    Validate",[2426,7571,2914],{"class":2442},[2426,7573,7574],{"class":2435}," error\n",[2426,7576,7577],{"class":2428,"line":31},[2426,7578,2472],{"class":2442},[3859,7580,220],{"id":7581},"adding-validation-to-types",[2338,7583,7584,7585,7588,7589,7592],{},"Implement ",[2423,7586,7587],{},"Validate()"," on your input or output types. Rocco integrates with the ",[2423,7590,7591],{},"check"," package for clean, composable validation:",[2417,7594,7596],{"className":2419,"code":7595,"language":2421,"meta":25,"style":25},"import \"github.com/zoobz-io/check\"\n\ntype CreateUserInput struct {\n    Name  string `json:\"name\"`\n    Email string `json:\"email\"`\n}\n\nfunc (c CreateUserInput) Validate() error {\n    return check.All(\n        check.Required(c.Name, \"name\"),\n        check.Email(c.Email, \"email\"),\n    )\n}\n",[2423,7597,7598,7605,7609,7619,7627,7635,7639,7643,7665,7680,7707,7730,7735],{"__ignoreMap":25},[2426,7599,7600,7602],{"class":2428,"line":9},[2426,7601,2796],{"class":2431},[2426,7603,7604],{"class":2455}," \"github.com/zoobz-io/check\"\n",[2426,7606,7607],{"class":2428,"line":20},[2426,7608,2479],{"emptyLinePlaceholder":2478},[2426,7610,7611,7613,7615,7617],{"class":2428,"line":31},[2426,7612,2432],{"class":2431},[2426,7614,2436],{"class":2435},[2426,7616,2439],{"class":2431},[2426,7618,2443],{"class":2442},[2426,7620,7621,7623,7625],{"class":2428,"line":844},[2426,7622,2449],{"class":2448},[2426,7624,2452],{"class":2435},[2426,7626,2513],{"class":2455},[2426,7628,7629,7631,7633],{"class":2428,"line":2475},[2426,7630,2461],{"class":2448},[2426,7632,2464],{"class":2435},[2426,7634,2523],{"class":2455},[2426,7636,7637],{"class":2428,"line":2482},[2426,7638,2472],{"class":2442},[2426,7640,7641],{"class":2428,"line":2494},[2426,7642,2479],{"emptyLinePlaceholder":2478},[2426,7644,7645,7647,7649,7652,7654,7656,7659,7661,7663],{"class":2428,"line":2506},[2426,7646,2908],{"class":2431},[2426,7648,2618],{"class":2442},[2426,7650,7651],{"class":2597},"c ",[2426,7653,2558],{"class":2435},[2426,7655,2629],{"class":2442},[2426,7657,7658],{"class":2552}," Validate",[2426,7660,2914],{"class":2442},[2426,7662,2626],{"class":2435},[2426,7664,2443],{"class":2442},[2426,7666,7667,7669,7672,7674,7677],{"class":2428,"line":2516},[2426,7668,4808],{"class":2601},[2426,7670,7671],{"class":2539}," check",[2426,7673,2549],{"class":2442},[2426,7675,7676],{"class":2552},"All",[2426,7678,7679],{"class":2442},"(\n",[2426,7681,7682,7685,7687,7690,7692,7695,7697,7699,7701,7704],{"class":2428,"line":2526},[2426,7683,7684],{"class":2539},"        check",[2426,7686,2549],{"class":2442},[2426,7688,7689],{"class":2552},"Required",[2426,7691,2594],{"class":2442},[2426,7693,7694],{"class":2539},"c",[2426,7696,2549],{"class":2442},[2426,7698,2677],{"class":2539},[2426,7700,2561],{"class":2442},[2426,7702,7703],{"class":2455}," \"name\"",[2426,7705,7706],{"class":2442},"),\n",[2426,7708,7709,7711,7713,7715,7717,7719,7721,7723,7725,7728],{"class":2428,"line":2531},[2426,7710,7684],{"class":2539},[2426,7712,2549],{"class":2442},[2426,7714,2699],{"class":2552},[2426,7716,2594],{"class":2442},[2426,7718,7694],{"class":2539},[2426,7720,2549],{"class":2442},[2426,7722,2699],{"class":2539},[2426,7724,2561],{"class":2442},[2426,7726,7727],{"class":2455}," \"email\"",[2426,7729,7706],{"class":2442},[2426,7731,7732],{"class":2428,"line":2536},[2426,7733,7734],{"class":2442},"    )\n",[2426,7736,7737],{"class":2428,"line":2569},[2426,7738,2472],{"class":2442},[2338,7740,7741,7742,7744],{},"When a handler uses ",[2423,7743,2558],{},", validation runs automatically after JSON parsing. No additional configuration needed.",[3859,7746,225],{"id":7747},"using-check-validators",[2338,7749,4581,7750,7752],{},[2423,7751,7591],{}," package provides many built-in validators:",[2417,7754,7756],{"className":2419,"code":7755,"language":2421,"meta":25,"style":25},"import \"github.com/zoobz-io/check\"\n\ntype CreateUserInput struct {\n    Name     string `json:\"name\"`\n    Email    string `json:\"email\"`\n    Age      int    `json:\"age\"`\n    Website  string `json:\"website,omitempty\"`\n}\n\nfunc (c CreateUserInput) Validate() error {\n    return check.All(\n        check.Required(c.Name, \"name\"),\n        check.MinLength(c.Name, 2, \"name\"),\n        check.MaxLength(c.Name, 100, \"name\"),\n        check.Required(c.Email, \"email\"),\n        check.Email(c.Email, \"email\"),\n        check.Min(c.Age, 0, \"age\"),\n        check.Max(c.Age, 150, \"age\"),\n        check.URL(c.Website, \"website\"), // optional field, only validated if non-empty\n    )\n}\n",[2423,7757,7758,7764,7768,7778,7787,7795,7806,7816,7820,7824,7844,7856,7878,7906,7934,7956,7978,8008,8036,8065,8069],{"__ignoreMap":25},[2426,7759,7760,7762],{"class":2428,"line":9},[2426,7761,2796],{"class":2431},[2426,7763,7604],{"class":2455},[2426,7765,7766],{"class":2428,"line":20},[2426,7767,2479],{"emptyLinePlaceholder":2478},[2426,7769,7770,7772,7774,7776],{"class":2428,"line":31},[2426,7771,2432],{"class":2431},[2426,7773,2436],{"class":2435},[2426,7775,2439],{"class":2431},[2426,7777,2443],{"class":2442},[2426,7779,7780,7782,7785],{"class":2428,"line":844},[2426,7781,2449],{"class":2448},[2426,7783,7784],{"class":2435},"     string",[2426,7786,2513],{"class":2455},[2426,7788,7789,7791,7793],{"class":2428,"line":2475},[2426,7790,2461],{"class":2448},[2426,7792,2500],{"class":2435},[2426,7794,2523],{"class":2455},[2426,7796,7797,7800,7803],{"class":2428,"line":2482},[2426,7798,7799],{"class":2448},"    Age",[2426,7801,7802],{"class":2435},"      int",[2426,7804,7805],{"class":2455},"    `json:\"age\"`\n",[2426,7807,7808,7811,7813],{"class":2428,"line":2494},[2426,7809,7810],{"class":2448},"    Website",[2426,7812,2452],{"class":2435},[2426,7814,7815],{"class":2455}," `json:\"website,omitempty\"`\n",[2426,7817,7818],{"class":2428,"line":2506},[2426,7819,2472],{"class":2442},[2426,7821,7822],{"class":2428,"line":2516},[2426,7823,2479],{"emptyLinePlaceholder":2478},[2426,7825,7826,7828,7830,7832,7834,7836,7838,7840,7842],{"class":2428,"line":2526},[2426,7827,2908],{"class":2431},[2426,7829,2618],{"class":2442},[2426,7831,7651],{"class":2597},[2426,7833,2558],{"class":2435},[2426,7835,2629],{"class":2442},[2426,7837,7658],{"class":2552},[2426,7839,2914],{"class":2442},[2426,7841,2626],{"class":2435},[2426,7843,2443],{"class":2442},[2426,7845,7846,7848,7850,7852,7854],{"class":2428,"line":2531},[2426,7847,4808],{"class":2601},[2426,7849,7671],{"class":2539},[2426,7851,2549],{"class":2442},[2426,7853,7676],{"class":2552},[2426,7855,7679],{"class":2442},[2426,7857,7858,7860,7862,7864,7866,7868,7870,7872,7874,7876],{"class":2428,"line":2536},[2426,7859,7684],{"class":2539},[2426,7861,2549],{"class":2442},[2426,7863,7689],{"class":2552},[2426,7865,2594],{"class":2442},[2426,7867,7694],{"class":2539},[2426,7869,2549],{"class":2442},[2426,7871,2677],{"class":2539},[2426,7873,2561],{"class":2442},[2426,7875,7703],{"class":2455},[2426,7877,7706],{"class":2442},[2426,7879,7880,7882,7884,7887,7889,7891,7893,7895,7897,7900,7902,7904],{"class":2428,"line":2569},[2426,7881,7684],{"class":2539},[2426,7883,2549],{"class":2442},[2426,7885,7886],{"class":2552},"MinLength",[2426,7888,2594],{"class":2442},[2426,7890,7694],{"class":2539},[2426,7892,2549],{"class":2442},[2426,7894,2677],{"class":2539},[2426,7896,2561],{"class":2442},[2426,7898,7899],{"class":3199}," 2",[2426,7901,2561],{"class":2442},[2426,7903,7703],{"class":2455},[2426,7905,7706],{"class":2442},[2426,7907,7908,7910,7912,7915,7917,7919,7921,7923,7925,7928,7930,7932],{"class":2428,"line":2588},[2426,7909,7684],{"class":2539},[2426,7911,2549],{"class":2442},[2426,7913,7914],{"class":2552},"MaxLength",[2426,7916,2594],{"class":2442},[2426,7918,7694],{"class":2539},[2426,7920,2549],{"class":2442},[2426,7922,2677],{"class":2539},[2426,7924,2561],{"class":2442},[2426,7926,7927],{"class":3199}," 100",[2426,7929,2561],{"class":2442},[2426,7931,7703],{"class":2455},[2426,7933,7706],{"class":2442},[2426,7935,7936,7938,7940,7942,7944,7946,7948,7950,7952,7954],{"class":2428,"line":2634},[2426,7937,7684],{"class":2539},[2426,7939,2549],{"class":2442},[2426,7941,7689],{"class":2552},[2426,7943,2594],{"class":2442},[2426,7945,7694],{"class":2539},[2426,7947,2549],{"class":2442},[2426,7949,2699],{"class":2539},[2426,7951,2561],{"class":2442},[2426,7953,7727],{"class":2455},[2426,7955,7706],{"class":2442},[2426,7957,7958,7960,7962,7964,7966,7968,7970,7972,7974,7976],{"class":2428,"line":2645},[2426,7959,7684],{"class":2539},[2426,7961,2549],{"class":2442},[2426,7963,2699],{"class":2552},[2426,7965,2594],{"class":2442},[2426,7967,7694],{"class":2539},[2426,7969,2549],{"class":2442},[2426,7971,2699],{"class":2539},[2426,7973,2561],{"class":2442},[2426,7975,7727],{"class":2455},[2426,7977,7706],{"class":2442},[2426,7979,7980,7982,7984,7987,7989,7991,7993,7996,7998,8001,8003,8006],{"class":2428,"line":2659},[2426,7981,7684],{"class":2539},[2426,7983,2549],{"class":2442},[2426,7985,7986],{"class":2552},"Min",[2426,7988,2594],{"class":2442},[2426,7990,7694],{"class":2539},[2426,7992,2549],{"class":2442},[2426,7994,7995],{"class":2539},"Age",[2426,7997,2561],{"class":2442},[2426,7999,8000],{"class":3199}," 0",[2426,8002,2561],{"class":2442},[2426,8004,8005],{"class":2455}," \"age\"",[2426,8007,7706],{"class":2442},[2426,8009,8010,8012,8014,8017,8019,8021,8023,8025,8027,8030,8032,8034],{"class":2428,"line":2682},[2426,8011,7684],{"class":2539},[2426,8013,2549],{"class":2442},[2426,8015,8016],{"class":2552},"Max",[2426,8018,2594],{"class":2442},[2426,8020,7694],{"class":2539},[2426,8022,2549],{"class":2442},[2426,8024,7995],{"class":2539},[2426,8026,2561],{"class":2442},[2426,8028,8029],{"class":3199}," 150",[2426,8031,2561],{"class":2442},[2426,8033,8005],{"class":2455},[2426,8035,7706],{"class":2442},[2426,8037,8038,8040,8042,8045,8047,8049,8051,8054,8056,8059,8062],{"class":2428,"line":2704},[2426,8039,7684],{"class":2539},[2426,8041,2549],{"class":2442},[2426,8043,8044],{"class":2552},"URL",[2426,8046,2594],{"class":2442},[2426,8048,7694],{"class":2539},[2426,8050,2549],{"class":2442},[2426,8052,8053],{"class":2539},"Website",[2426,8055,2561],{"class":2442},[2426,8057,8058],{"class":2455}," \"website\"",[2426,8060,8061],{"class":2442},"),",[2426,8063,8064],{"class":2944}," // optional field, only validated if non-empty\n",[2426,8066,8067],{"class":2428,"line":2713},[2426,8068,7734],{"class":2442},[2426,8070,8071],{"class":2428,"line":2719},[2426,8072,2472],{"class":2442},[3859,8074,230],{"id":8075},"output-validation",[2338,8077,8078,8079,2651],{},"Output validation is opt-in for performance. Enable with ",[2423,8080,8081],{},"WithOutputValidation()",[2417,8083,8085],{"className":2419,"code":8084,"language":2421,"meta":25,"style":25},"handler := rocco.POST[Input, Output](\"/path\", fn).\n    WithOutputValidation() // Validates output if Output implements Validatable\n",[2423,8086,8087,8121],{"__ignoreMap":25},[2426,8088,8089,8091,8093,8095,8097,8100,8102,8104,8106,8108,8111,8114,8116,8119],{"class":2428,"line":9},[2426,8090,2540],{"class":2539},[2426,8092,2543],{"class":2539},[2426,8094,2546],{"class":2539},[2426,8096,2549],{"class":2442},[2426,8098,8099],{"class":2552},"POST",[2426,8101,2555],{"class":2442},[2426,8103,7236],{"class":2435},[2426,8105,2561],{"class":2442},[2426,8107,7065],{"class":2435},[2426,8109,8110],{"class":2442},"](",[2426,8112,8113],{"class":2455},"\"/path\"",[2426,8115,2561],{"class":2442},[2426,8117,8118],{"class":2539}," fn",[2426,8120,3175],{"class":2442},[2426,8122,8123,8126,8128],{"class":2428,"line":20},[2426,8124,8125],{"class":2552},"    WithOutputValidation",[2426,8127,2914],{"class":2442},[2426,8129,8130],{"class":2944}," // Validates output if Output implements Validatable\n",[3859,8132,235],{"id":8133},"validation-tags-for-openapi",[2338,8135,4581,8136,8139],{},[2423,8137,8138],{},"validate"," struct tag is still parsed for OpenAPI schema generation, even without runtime validation:",[2417,8141,8143],{"className":2419,"code":8142,"language":2421,"meta":25,"style":25},"type CreateUserInput struct {\n    Name  string `json:\"name\" validate:\"required,min=2,max=100\"`\n    Email string `json:\"email\" validate:\"required,email\"`\n}\n",[2423,8144,8145,8155,8164,8172],{"__ignoreMap":25},[2426,8146,8147,8149,8151,8153],{"class":2428,"line":9},[2426,8148,2432],{"class":2431},[2426,8150,2436],{"class":2435},[2426,8152,2439],{"class":2431},[2426,8154,2443],{"class":2442},[2426,8156,8157,8159,8161],{"class":2428,"line":20},[2426,8158,2449],{"class":2448},[2426,8160,2452],{"class":2435},[2426,8162,8163],{"class":2455}," `json:\"name\" validate:\"required,min=2,max=100\"`\n",[2426,8165,8166,8168,8170],{"class":2428,"line":31},[2426,8167,2461],{"class":2448},[2426,8169,2464],{"class":2435},[2426,8171,2467],{"class":2455},[2426,8173,8174],{"class":2428,"line":844},[2426,8175,2472],{"class":2442},[2338,8177,8178,8179,3583,8182,3583,8185,8188],{},"This generates OpenAPI constraints (",[2423,8180,8181],{},"minLength",[2423,8183,8184],{},"maxLength",[2423,8186,8187],{},"format: email",") in your spec.",[3859,8190,240],{"id":8191},"validation-errors",[2338,8193,8194],{},"Invalid inputs return 422 Unprocessable Entity with detailed field errors:",[2417,8196,8198],{"className":3733,"code":8197,"language":3735,"meta":25,"style":25},"{\n  \"code\": \"VALIDATION_FAILED\",\n  \"message\": \"validation failed\",\n  \"details\": {\n    \"fields\": [\n      {\"field\": \"email\", \"message\": \"must be a valid email address\"}\n    ]\n  }\n}\n",[2423,8199,8200,8204,8214,8224,8230,8236,8258,8262,8266],{"__ignoreMap":25},[2426,8201,8202],{"class":2428,"line":9},[2426,8203,2642],{"class":2799},[2426,8205,8206,8208,8210,8212],{"class":2428,"line":20},[2426,8207,3746],{"class":2597},[2426,8209,3566],{"class":2799},[2426,8211,3751],{"class":2455},[2426,8213,2585],{"class":2799},[2426,8215,8216,8218,8220,8222],{"class":2428,"line":31},[2426,8217,3758],{"class":2597},[2426,8219,3566],{"class":2799},[2426,8221,3763],{"class":2455},[2426,8223,2585],{"class":2799},[2426,8225,8226,8228],{"class":2428,"line":844},[2426,8227,3770],{"class":2597},[2426,8229,3773],{"class":2799},[2426,8231,8232,8234],{"class":2428,"line":2475},[2426,8233,3778],{"class":2597},[2426,8235,3781],{"class":2799},[2426,8237,8238,8240,8242,8244,8247,8249,8251,8253,8256],{"class":2428,"line":2482},[2426,8239,3786],{"class":2799},[2426,8241,3789],{"class":2597},[2426,8243,3566],{"class":2799},[2426,8245,8246],{"class":2455},"\"email\"",[2426,8248,3583],{"class":2799},[2426,8250,3799],{"class":2597},[2426,8252,3566],{"class":2799},[2426,8254,8255],{"class":2455},"\"must be a valid email address\"",[2426,8257,2472],{"class":2799},[2426,8259,8260],{"class":2428,"line":2494},[2426,8261,3834],{"class":2799},[2426,8263,8264],{"class":2428,"line":2506},[2426,8265,3839],{"class":2799},[2426,8267,8268],{"class":2428,"line":2516},[2426,8269,2472],{"class":2799},[2412,8271,245],{"id":8272},"identity",[2338,8274,8275,8276,8278],{},"Identity represents an authenticated user or service. Implement the ",[2423,8277,245],{}," interface:",[2417,8280,8282],{"className":2419,"code":8281,"language":2421,"meta":25,"style":25},"type Identity interface {\n    ID() string              // Unique identifier\n    TenantID() string        // Tenant/organization ID\n    Scopes() []string        // Permission scopes\n    Roles() []string         // User roles\n    Stats() map[string]int   // Usage statistics\n    HasScope(string) bool    // Check scope\n    HasRole(string) bool     // Check role\n}\n",[2423,8283,8284,8294,8305,8317,8332,8346,8368,8385,8401],{"__ignoreMap":25},[2426,8285,8286,8288,8290,8292],{"class":2428,"line":9},[2426,8287,2432],{"class":2431},[2426,8289,6750],{"class":2435},[2426,8291,7562],{"class":2431},[2426,8293,2443],{"class":2442},[2426,8295,8296,8298,8300,8302],{"class":2428,"line":20},[2426,8297,2497],{"class":2552},[2426,8299,2914],{"class":2442},[2426,8301,2464],{"class":2435},[2426,8303,8304],{"class":2944},"              // Unique identifier\n",[2426,8306,8307,8310,8312,8314],{"class":2428,"line":31},[2426,8308,8309],{"class":2552},"    TenantID",[2426,8311,2914],{"class":2442},[2426,8313,2464],{"class":2435},[2426,8315,8316],{"class":2944},"        // Tenant/organization ID\n",[2426,8318,8319,8322,8324,8327,8329],{"class":2428,"line":844},[2426,8320,8321],{"class":2552},"    Scopes",[2426,8323,2914],{"class":2442},[2426,8325,8326],{"class":2442}," []",[2426,8328,4455],{"class":2435},[2426,8330,8331],{"class":2944},"        // Permission scopes\n",[2426,8333,8334,8337,8339,8341,8343],{"class":2428,"line":2475},[2426,8335,8336],{"class":2552},"    Roles",[2426,8338,2914],{"class":2442},[2426,8340,8326],{"class":2442},[2426,8342,4455],{"class":2435},[2426,8344,8345],{"class":2944},"         // User roles\n",[2426,8347,8348,8351,8353,8356,8358,8360,8362,8365],{"class":2428,"line":2482},[2426,8349,8350],{"class":2552},"    Stats",[2426,8352,2914],{"class":2442},[2426,8354,8355],{"class":2431}," map",[2426,8357,2555],{"class":2442},[2426,8359,4455],{"class":2435},[2426,8361,6682],{"class":2442},[2426,8363,8364],{"class":2435},"int",[2426,8366,8367],{"class":2944},"   // Usage statistics\n",[2426,8369,8370,8373,8375,8377,8379,8382],{"class":2428,"line":2494},[2426,8371,8372],{"class":2552},"    HasScope",[2426,8374,2594],{"class":2442},[2426,8376,4455],{"class":2435},[2426,8378,2629],{"class":2442},[2426,8380,8381],{"class":2435}," bool",[2426,8383,8384],{"class":2944},"    // Check scope\n",[2426,8386,8387,8390,8392,8394,8396,8398],{"class":2428,"line":2506},[2426,8388,8389],{"class":2552},"    HasRole",[2426,8391,2594],{"class":2442},[2426,8393,4455],{"class":2435},[2426,8395,2629],{"class":2442},[2426,8397,8381],{"class":2435},[2426,8399,8400],{"class":2944},"     // Check role\n",[2426,8402,8403],{"class":2428,"line":2516},[2426,8404,2472],{"class":2442},[2338,8406,8407,8408,2651],{},"Configure identity extraction via ",[2423,8409,1591],{},[2417,8411,8413],{"className":2419,"code":8412,"language":2421,"meta":25,"style":25},"engine := rocco.NewEngine().WithAuthenticator(func(ctx context.Context, r *http.Request) (rocco.Identity, error) {\n    token := r.Header.Get(\"Authorization\")\n    // Validate token, return identity or error\n    return &MyIdentity{...}, nil\n})\n",[2423,8414,8415,8475,8497,8502,8519],{"__ignoreMap":25},[2426,8416,8417,8419,8421,8423,8425,8427,8429,8431,8433,8435,8437,8439,8441,8443,8445,8447,8449,8451,8453,8455,8457,8459,8461,8463,8465,8467,8469,8471,8473],{"class":2428,"line":9},[2426,8418,4353],{"class":2539},[2426,8420,2543],{"class":2539},[2426,8422,2546],{"class":2539},[2426,8424,2549],{"class":2442},[2426,8426,1582],{"class":2552},[2426,8428,4364],{"class":2442},[2426,8430,1591],{"class":2552},[2426,8432,2594],{"class":2442},[2426,8434,2908],{"class":2431},[2426,8436,2594],{"class":2442},[2426,8438,4601],{"class":2597},[2426,8440,4604],{"class":2435},[2426,8442,2549],{"class":2442},[2426,8444,4609],{"class":2435},[2426,8446,2561],{"class":2442},[2426,8448,4614],{"class":2597},[2426,8450,2602],{"class":2601},[2426,8452,4619],{"class":2435},[2426,8454,2549],{"class":2442},[2426,8456,180],{"class":2435},[2426,8458,2629],{"class":2442},[2426,8460,2618],{"class":2442},[2426,8462,2336],{"class":2435},[2426,8464,2549],{"class":2442},[2426,8466,245],{"class":2435},[2426,8468,2561],{"class":2442},[2426,8470,2626],{"class":2435},[2426,8472,2629],{"class":2442},[2426,8474,2443],{"class":2442},[2426,8476,8477,8479,8481,8483,8485,8487,8489,8491,8493,8495],{"class":2428,"line":20},[2426,8478,4651],{"class":2539},[2426,8480,2543],{"class":2539},[2426,8482,4614],{"class":2539},[2426,8484,2549],{"class":2442},[2426,8486,4660],{"class":2539},[2426,8488,2549],{"class":2442},[2426,8490,4665],{"class":2552},[2426,8492,2594],{"class":2442},[2426,8494,4670],{"class":2455},[2426,8496,2743],{"class":2442},[2426,8498,8499],{"class":2428,"line":31},[2426,8500,8501],{"class":2944},"    // Validate token, return identity or error\n",[2426,8503,8504,8506,8508,8511,8513,8515,8517],{"class":2428,"line":844},[2426,8505,4808],{"class":2601},[2426,8507,4811],{"class":2601},[2426,8509,8510],{"class":2435},"MyIdentity",[2426,8512,4458],{"class":2442},[2426,8514,6263],{"class":2601},[2426,8516,6266],{"class":2442},[2426,8518,2710],{"class":2431},[2426,8520,8521],{"class":2428,"line":2475},[2426,8522,5084],{"class":2442},[2412,8524,93],{"id":8525},"next-steps",[3420,8527,8528,8534,8541],{},[3423,8529,8530,8533],{},[2341,8531,258],{"href":8532},"architecture"," - How rocco processes requests internally",[3423,8535,8536,8540],{},[2341,8537,8539],{"href":8538},"../guides/handlers","Handler Guide"," - Advanced handler patterns",[3423,8542,8543,8546],{},[2341,8544,457],{"href":8545},"../guides/errors"," - Error patterns and custom errors",[2412,8548,98],{"id":8549},"see-also",[3420,8551,8552,8559],{},[3423,8553,8554,8558],{},[2341,8555,8557],{"href":8556},"../guides/authentication","Authentication Guide"," - Identity and authorization",[3423,8560,8561,8564],{},[2341,8562,1561],{"href":8563},"../reference/api"," - Complete API documentation",[3993,8566,8567],{},"html pre.shiki code .sLkEo, html code.shiki .sLkEo{--shiki-default:var(--shiki-comment)}html pre.shiki code .sh8_p, html code.shiki .sh8_p{--shiki-default:var(--shiki-text)}html pre.shiki code .sq5bi, html code.shiki .sq5bi{--shiki-default:var(--shiki-punctuation)}html pre.shiki code .s5klm, html code.shiki .s5klm{--shiki-default:var(--shiki-function)}html pre.shiki code .sMAmT, html code.shiki .sMAmT{--shiki-default:var(--shiki-number)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sYBwO, html code.shiki .sYBwO{--shiki-default:var(--shiki-type)}html pre.shiki code .sxAnc, html code.shiki .sxAnc{--shiki-default:var(--shiki-string)}html pre.shiki code .sUt3r, html code.shiki .sUt3r{--shiki-default:var(--shiki-keyword)}html pre.shiki code .sSYET, html code.shiki .sSYET{--shiki-default:var(--shiki-parameter)}html pre.shiki code .sW3Qg, html code.shiki .sW3Qg{--shiki-default:var(--shiki-operator)}html pre.shiki code .sBGCq, html code.shiki .sBGCq{--shiki-default:var(--shiki-property)}html pre.shiki code .soy-K, html code.shiki .soy-K{--shiki-default:#BBBBBB}",{"title":25,"searchDepth":20,"depth":20,"links":8569},[8570,8571,8575,8579,8583,8591,8592,8593],{"id":4353,"depth":20,"text":160},{"id":2540,"depth":20,"text":165,"children":8572},[8573,8574],{"id":6279,"depth":31,"text":170},{"id":6530,"depth":31,"text":175},{"id":6656,"depth":20,"text":180,"children":8576},[8577,8578],{"id":6760,"depth":31,"text":185},{"id":7027,"depth":31,"text":190},{"id":7205,"depth":20,"text":195,"children":8580},[8581,8582],{"id":7210,"depth":31,"text":200},{"id":7353,"depth":31,"text":205},{"id":7538,"depth":20,"text":210,"children":8584},[8585,8586,8587,8588,8589,8590],{"id":7547,"depth":31,"text":215},{"id":7581,"depth":31,"text":220},{"id":7747,"depth":31,"text":225},{"id":8075,"depth":31,"text":230},{"id":8133,"depth":31,"text":235},{"id":8191,"depth":31,"text":240},{"id":8272,"depth":20,"text":245},{"id":8525,"depth":20,"text":93},{"id":8549,"depth":20,"text":98},{},"2025-12-16T00:00:00.000Z",null,{"title":151,"description":153},[3387,346,8599],"Requests","JULSL9gamuUE2nV7WgpMSTOqSDNMTU84T7oTwS3ZQbk",[8602,8603],{"title":103,"path":102,"stem":2251,"description":105,"children":-1},{"title":258,"path":257,"stem":2255,"description":260,"children":-1},1776121359931]