{"id":48908595,"url":"https://github.com/aminmal/stgin","last_synced_at":"2026-04-16T22:03:46.520Z","repository":{"id":39914073,"uuid":"489073322","full_name":"AminMal/stgin","owner":"AminMal","description":"Powerful functional RESTful API server","archived":false,"fork":false,"pushed_at":"2022-09-04T14:03:47.000Z","size":196,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-06-20T03:37:44.866Z","etag":null,"topics":["go","golang","rest","rest-api","restful","restful-api"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AminMal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-05-05T17:49:44.000Z","updated_at":"2024-06-20T03:37:44.867Z","dependencies_parsed_at":"2022-09-03T00:01:33.623Z","dependency_job_id":null,"html_url":"https://github.com/AminMal/stgin","commit_stats":null,"previous_names":[],"tags_count":59,"template":false,"template_full_name":null,"purl":"pkg:github/AminMal/stgin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AminMal%2Fstgin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AminMal%2Fstgin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AminMal%2Fstgin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AminMal%2Fstgin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AminMal","download_url":"https://codeload.github.com/AminMal/stgin/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AminMal%2Fstgin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31905895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-16T18:22:33.417Z","status":"ssl_error","status_checked_at":"2026-04-16T18:21:47.142Z","response_time":69,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","rest","rest-api","restful","restful-api"],"created_at":"2026-04-16T22:03:24.534Z","updated_at":"2026-04-16T22:03:46.502Z","avatar_url":"https://github.com/AminMal.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# STGIN\n\nstgin is a functional RESTful API server framework that provides easy APIs in order to maintain your application RESTful API server.\n\n# Installation\nYou can use the following command to add stgin into your application.\n```\ngo get github.com/AminMal/stgin\n```\n\n# Quick Start\nstgin concentrates a lot about making a well-structured application.\nLet's take a look at the following application structure, this is just a part of a simple application (health checking part).\n```\n  /project\n    ... other packages you might need (i.e., users, authentication, inventory, ...)\n    /health\n      init.go\n      models.go\n      apis.go\n      ... other things you might need\n    init.go\n    main.go\n```\nLet's start defining health APIs:\n```go\n// /health/apis.go file\n\npackage health\n\nimport \"github.com/AminMal/stgin\"\n// #recommended\nfunc getHealthAPI(request stgin.RequestContext) stgin.Status {\n    healthResponse := getHealth() // some method implemented in package\n    // model is defined earlier in models.go\n    if healthResponse.ok {\n        return stgin.Ok(stgin.Json(\u0026healthResponse))\n    } else {\n        return stgin.InternalServerError(stgin.Json(\u0026healthResponse))\n    }\n}\n// you can also define APIs as variable:\n// var someAPI stgin.API = func(request stgin.RequestContext) stgin.Status {...}\n\n// but using functions is more convenient and is recommended\n// or inline the implementation in route definition #not recommended\n\n```\nFine for now, let's create the controller in `/health/init.go`. It's better to use init to initialize controllers and apis.\n```go\n// health/init.go file\n\npackage health\n\nimport \"github.com/AminMal/stgin\"\n\nvar Controller *stgin.Controller // exposed to main package\n\nfunc init() {\n    Controller = stgin.NewController(\"HealthController\", \"/health\")\n    Controller.AddRoutes(\n      stgin.GET(\"/status\", getHealthAPI), // defined in apis.go\n      // this route will be interpreted to /health/status\n    )\n}\n```\nSo the health APIs are implemented, let's integrate them into the server.\nNow in the root directory of the project, let's open `init.go` file.\n```go\npackage main\n\nimport \"github.com/AminMal/stgin\"\nimport \"fmt\"\n\nvar server *stgin.Server\n\nfunc init() {\n        portNo := 9000\n        server = stgin.DefaultServer(fmt.Sprintf(\":%d\", portNo))\n        // default server included default logger and error handler\n        server.Register(health.Controller)\n}\n```\nAlmost done, let's just run the server (main.go).\n```go\npackage main\n\nimport \"log\"\n\nfunc main() {\n        log.Fatal(server.Start()) // all done\n}\n```\nYour application will be up and running.\n\nAnother approach to define routes is route builder. You might want to use some method which is not defined in stgin default routing methods.\nYou can use:\n```go\nstgin.OnPattern(\"/your/path/$path_param\").WithMethod(\"something\").Do(\n        func(req stgin.RequestContext) stgin.Status{...},\n)\n```\n\nA comparison between stgin and gin-gonic, writing a simple API:\n```go\n// Given response type as\ntype HealthCheckResponse struct {\n        DBConnection    bool    `json:\"database_connection\"`\n        Message         string  `json:\"message\"`\n}\n// and request type as\ntype HealthCheckRequest struct {\n        Whatever        string   `json:\"whatever\"`\n}\n```\n***stgin implementation:***\n```go\nhealth := stgin.POST(\"/health\", func(request stgin.RequestContext) stgin.Status {\n    var reqBody HealthCheckRequest\n    request.Body().JSONInto(\u0026reqBody)\n    // do something with reqBody\n    var response HealthCheckResponse = GetHealth()\n    if response.DBConnection {\n        return stgin.Ok(stgin.Json(\u0026response)) \n    } else {\n        return stgin.InternalServerError(stgin.Json(\u0026response))\n    }\n})\n```\n***gin implementation:***\n```go\nr.POST(\"/health\", func(c *framework.Context) {\n    var reqBody HealthCheckRequest\n    bodyBytes, err := ioutil.ReadAll(c.Request.Body)\n    if err != nil {\n        // potential error handling\n    }\n    err = json.Unmarshal(bodyBytes, \u0026reqBody)\n    if err != nil {\n        // potential error handling\n    }\n        // do something with reqBody\n    var response HealthCheckResponse = GetHealth()\n    jsonResponse, _ := json.Marshal(response)\n    if response.DBConnection {\n        _, writeError := c.Writer.Write(jsonResponse)\n        c.Status(200)\n        if writeError != nil {\n            // potential error handling\n        }\n    } else {\n        c.Status(500)\n        _, writeError = c.Writer.Write(jsonResponse)\n        if writeError != nil {\n            // potential error handling\n        }\n    }\n})\n```\nOr just easily add headers or cookies with a receiver function instead of manually writing:\n```go\nstgin.Ok(...).\n      WithHeaders(...).\n      WithCookies(...)\n```\n\n## Structure\n\nThe structure of stgin types and interfaces is pretty simple, a `Server` may have several `Controller`s, and each controller may have serveral `Route`s.\n```\n    \n    -Server =\u003e\n        -Controller 1 -\u003e\n            -Route 1 (path pattern, method, api)\n            -Route 2 (path pattern, method, api)\n        -Cotroller 2 -\u003e\n            -Route 1 (path pattern, method, api)\n```\n**Server:** Is run on the specified port, contains the controllers.\n\n**Controller:** Contains routes which are exposed to the server, has a name, and may have a route prefix (i.e., /home)\n\n**Route:** Holds route specifications (i.e., method, path, API action)\n\n**RequestContext**: Holds the information about the requests, such as uri, body, headers, ... . Can parse request entity into the desired variable, using helper functions like `request.JSONInto`, `request.XMLInto`.\n\n**Status:** Is a wrapper around an actual http response, holds status code, response headers, response body, ... (i.e., Ok, BadRequest, ...)\n* ResponseEntity: A response could be of different content types (i.e., JSON, XML, Text, file, ...). A response entity is an interface which defines the content type of the response, and entity bytes. There are some helper functions provided in stgin to ease the use of these entities, like `stgin.Json`, `stgin.Text`, `stgin.Xml`, `stgin.File`.\n\n**API:** Is a type alias for a function which accepts a request context and returns a status.\n\n# Path Parameters\n* How to define?\n\n    When defining a route, there are 2 possible values between any 2 slashes, a literal string (like \".../home/...\"), or a path parameter specification.\n    Path parameters must have a name, and an optional type which the parameter might have (string is used as default, if no type or an invalid type is provided).\n  \n    ```\n            //same as $username:string\n    stgin.GET(\"/users/$username/purchases/$purchase_id:int\". ...)\n  \n    // \"/users/$username/purchases/$purchase_id\" also accepts the correct route like \"/users/John/purchases/12\",\n    // but in case an alphanumeric value is passed as purchase_id, conversion from string to int in the api is manual\n    // and needs manual error checking\n    ```\n* How to get?\n    ```\n    username, exists := request.PathParams.Get(\"username\")\n    // or if you're sure about the existence, \n    username := request.PathParams.MustGet(\"username\")\n    // or if you have specified the type in path pattern\n    purchaseId := request.PathParams.MustGetInt(\"purchase_id\")\n    // or\n    purchaseId, err := request.PathParams.GetInt(\"purchase_id\")\n    ```\n  \n# Query Parameters\n* When to define?\n\n  Define query parameter specifications only for mandatory and non-empty query parameter expectations.\n\n\n* How to define?\n\n  When defining a route, you can specify which query params of what type the route should expect. If a request could not satisfy the expected queries, it will be rejected by the route and will be passed into the next route and so on.\n  Specifying query parameters does not mean that the route will not accept other query parameters which are not specified.\n  By specifying the query parameters, you just make sure that when a request is accepted by the route, it always contains those query parameters with the right type.\n  After defining the path pattern, use a question mark `?` to start defining query parameters, write the name of the parameter (if it has a certain type, use `:` and put the type name, i.e., int, string, float),\n  and when done, put `\u0026` to define the next query parameter. The order of queries does not matter.\n  ```go\n    stgin.GET(\"/users?username\u0026id:int\")\n    // Just like path parameters, if you do not specify the type of query, it's assumed to be string, \n    // so \"username\" here is interpreted into \"username:string\" \n   ```\n  As mentioned earlier, this pattern will match urls like `/users?id=7\u0026username=JohnDoe\u0026otherquery=whatever\u0026anotherone=true`. \n  And you can access those easily in the request, so no worries about not specifying all the query parameters.\n  \n* How to get?\n\n  Just like path parameters, query parameters follow the same rules for receiver functions.\n  ```go\n    userId := request.QueryParams.MustGetInt(\"user_id\")\n    // so on, just like path parameters\n  ```\n\n* Query to object\n\n  There is a special method in request context, which can convert queries into a struct object.\n  There are some few notes to take before using this. When defining the expected struct that the queries will be converted into,\n  if you need to use other naming in queries rather than the field name in struct, use `qp` (short for query parameter) tag to specify the name (just like json tag):\n  ```go\n    type UserSearchFilter struct {\n        Username   string  `qp:\"name\"`\n        Id         int     `qp:\"id\"`\n        Joined     string  \n    }\n  ```\n  * **Always pass pointers to the function**\n  * **Non-exported fields will not be parsed from request query parameters**\n  * **If you do not pass the name to qp tag, parser would look up for the actual field name in the queries:**\n    Notice the `Joined` field in the struct, parser looks for `\u0026Joined=...` in the url.\n    \n## Custom parameter patterns\nstgin provides patterns for `int`, `string`, `float` and `uuid` already. but If you need to add some custom pattern to use in query or path parameter definition, you can add the name with a valid pattern(regex) that gets compiled. \n```go\nstartsWithJohnRegex := \"^john.*\"\nerr := AddMatchingPattern(\"john\", startsWithJohnRegex)\n```\nthe above function will fail with errors if:\n* 1) Try to override default patterns (`int`, `string`, `float`, `uuid`)\n* 2) The given pattern couldn't be compiled\n-----\n## Custom Actions\nstgin does not provide actions about stuff like Authentication, because simple authentication is not useful most of the time, and you may need customized authentications.\n\nFor instance:\n```go\ntype AuthInfo struct {\n        Username    string      `json:\"username\"`\n        AccountId   int         `json:\"account_id\"`\n        Roles       []string    `json:\"roles\"`\n}\n\nfunc authenticate(rc stgin.RequestContext) (AuthInfo, bool) {\n    if name, found := rc.QueryParams.Get(\"user\"); !found {\n        ...\n    } else {\n        ...\n    }\n}\n\nfunc Authenticated(rc stgin.RequestContext, action func(AuthInfo) stgin.Status) stgin.Status {\n    authInfo, isAuthenticated := authenticate(rc)\n    if !isAuthenticated {\n        return stgin.Unauthorized(...)\n    } else {\n        return action(authInfo)\n    }\n}\n\n// In the apis section\nmyAPI := stgin.GET(\"/test\", func(request stgin.RequestContext) stgin.Status {\n    return Authenticated(request, func(authInfo AuthInfo) stgin.Status {\n        return stgin.Ok(stgin.Json(\u0026Greet(authInfo.Username)))\n    })\n})\n\n```\n\n# Listeners\nListeners are functions, which can affect the request and response based on the defined behavior.\nFor instance, a `ResponseListener` is a function which receives a response, and returns a response, it can be used when you want to apply something to all the responses in server layer or controller layer (i.e., providing CORS headers).\nThere are 3 types of listeners:\n* RequestListener: `RequestContext =\u003e RequestContext` [Can be used to return a new modified instance of the request]\n\n  ```go\n    func AddUserTrackingKey(request stgin.RequestContext) stgin.RequestContext {\n      request.Headers[\"X-Tracking-Key\"] = []string{\"some-generated-header\"}\n      return request\n    }\n  ```\n* ResponseListener: `Status =\u003e Status` [Can be used to add/remove additional information to a new instance of Status]\n\n  ```go\n    func AddUserKeyToResponse(response stgin.Status) stgin.Status {\n      return response.WithHeaders(http.Header{\"X-Tracking-Key\": {\"\u003cSome random value\u003e\"}}\n    }\n  ```\n* APIListener: `(RequestContext, Status) =\u003e void` [Can be used to do stuff like logging, ...]\n\n  ```go\n    func ApiLogger(request stgin.RequestContext, response stgin.Status) {\n      fmt.Println(\"received\", request, \"returned\", response)\n    }\n  ```\n  There are some listeners provided inside the stgin package which can be used inside a server or a controller (API watcher/logger, recovery, they're used in `stgin.DefaultServer` as well).\n\n  \n# CORS Handling\nCors handling can be done using server APIs:\n```go\nserver.CorsHandler(stgin.CorsHandler{\n        AllowOrigin: []string{\"first_host.com\", \"second_host.com\"},\n        AllowHeaders: []string{\"*\"},\n        AllowMethods: []string{\"GET\", \"PUT\", \"POST\", \"HEAD\", \"OPTIONS\"},\n        ...\n})\n```\n\n\n# Timeout\nYou can adjust server request timeout using `server.SetTimeout`,\nthis way if the request processing takes longer than the specified timeout,\nthe server will automatically abort the request and complete with a `408 request timed out` response.\n\n# Custom Recovery\nAn `ErrorHandler` can be provided by the developer, to provide custom error handling behavior.\nDefinition of an `ErrorHandler` function is pretty straight forward, you just define a function which takes the request and the error, and decides what to return as the status.\n```go\nvar myErrorHandler stgin.ErrorHandler = func(request RequestContext, err any) stgin.Status {\n    if myCustomErr, isCustomErr := err.(CustomErr); isCustomErr {\n        return stgin.BadRequest(...)\n    } else {\n        return stgin.InternalServerError(...)\n    }\n}\n```\n\n# Files And Directories\n**Files:** \n\nWorking with files and directories is pretty easy. \nThey are dealt just as normal response entities. They have a content type depending on the file format, and file bytes.\nSo you can return them inside your APIs, just give stgin the file location. If the file could not be found, `404 not found` is returned to the client as the response, and if there was some problems reading the file, `500 internal server error` would be returned.\n\n**Directories:**\n\nDirectories are a bit out of RESTful APIs concept, so It's not possible in stgin to return them as an http response.\nInstead of defining routes for the file system, a special Routing function is available as `StaticDir`:\n```go\nSomeController.AddRoutes(...) // some routes\nSomeController.AddRoutes(\n    stgin.GET(\"/some/pattern\", someAPI),\n    stgin.StaticDir(\"/get_my_files\", \"/tmp\"),\n    // serves /tmp folder on \"\u003c/controller_prefix_if_exists\u003e/get_my_files\"\n)\n```\n# Http 2 Push\nHttp push is available if you're using go 1.18 above, and using http 2 as a communication protocol.\n```go\n// inside api definiction\nif request.HttpPush.IsSupported {\n        pusher := request.HttpPush.Pusher\n        // do stuff with pusher\n}\n```\n\n# Html Templates\nSTgin does not support \"rendering\" html templates (parsing and creating appropriate sources for images, etc. developers should take care of images),\nbut loading html templates using variables is supported.\n\n* **Template variables:** To define template variables, wrap the variable name inside double curly braces. (i.e., {{ title }}, {{name}}, spaces are ignored).\n\n```go\n//    /\n//    /templates\n//      welcome.html\n//      /images\nstgin.GET(\"/welcome\", func (request stgin.RequestContext) stgin.Status {\n    return stgin.Ok(template.LoadTemplate(\"./templates/welcome.html\", template.Variables{\n        \"title\": \"Welcome to the team\",\n        \"name\": \"John Doe\",\n}))\n})\n```\nWIP","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faminmal%2Fstgin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faminmal%2Fstgin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faminmal%2Fstgin/lists"}