{"id":18256586,"url":"https://github.com/honmaple/forest","last_synced_at":"2025-04-04T18:30:26.074Z","repository":{"id":40603868,"uuid":"443113659","full_name":"honmaple/forest","owner":"honmaple","description":"lightweight and fast HTTP router written in Go","archived":false,"fork":false,"pushed_at":"2024-08-29T03:00:34.000Z","size":3446,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-20T16:51:57.779Z","etag":null,"topics":["go","golang","http","router","web-framework"],"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/honmaple.png","metadata":{"files":{"readme":"README.org","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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-30T15:29:59.000Z","updated_at":"2024-08-29T03:00:38.000Z","dependencies_parsed_at":"2024-11-05T10:51:18.218Z","dependency_job_id":null,"html_url":"https://github.com/honmaple/forest","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/honmaple%2Fforest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/honmaple%2Fforest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/honmaple%2Fforest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/honmaple%2Fforest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/honmaple","download_url":"https://codeload.github.com/honmaple/forest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247229080,"owners_count":20904976,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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","http","router","web-framework"],"created_at":"2024-11-05T10:22:41.570Z","updated_at":"2025-04-04T18:30:25.522Z","avatar_url":"https://github.com/honmaple.png","language":"Go","readme":"** Example\n   #+begin_src go\n     package main\n\n     import (\n         \"net/http\"\n\n         \"github.com/honmaple/forest\"\n         \"github.com/honmaple/forest/middleware\"\n     )\n\n     func main() {\n         r := forest.New(forest.Debug())\n         r.Use(middleware.Recover())\n         r.Use(middleware.Logger())\n         r.GET(\"/\", func(c forest.Context) error {\n             return c.HTML(http.StatusOK, \"\u003ch1\u003eHello Forest\u003c/h1\u003e\")\n         })\n\n         v1 := r.Group(forest.WithPrefix(\"/api\"))\n         {\n             v1.GET(\"/posts/{title}\", func(c forest.Context) error {\n                 return c.JSON(http.StatusOK, forest.H{\"title\": c.Param(\"title\")})\n             })\n             v1.POST(\"/posts\", func(c forest.Context) error {\n                 type post struct {\n                     Title   string `json:\"title\"   form:\"title\"`\n                     Content string `json:\"content\" form:\"content\"`\n                 }\n                 p := post{}\n                 if err := c.Bind(\u0026p); err != nil {\n                     return c.JSON(http.BadRequest, forest.H{\"message\": err.Error()})\n                 }\n                 return c.JSON(http.StatusOK, p)\n             })\n         }\n\n         v2 := forest.NewGroup(forest.WithHost(\"v2.localhost:8000\"))\n         {\n             v2.GET(\"/posts/{title}\", func(c forest.Context) error {\n                 return c.JSON(http.StatusOK, forest.H{\"title\": c.Param(\"title\")})\n             })\n         }\n\n         r.Mount(v2)\n         r.Start(\"127.0.0.1:8000\")\n     }\n   #+end_src\n\n** Route\n\n*** Single parameter in path\n    #+begin_src go\n      router := forest.New()\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 {\"var\": \"test\"}\n      // /posts, /posts/, /posts/1/1 not match\n      router.GET(\"/posts/:var\", handler)\n      // /posts/                     {\"var\": \"\"}\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 {\"var\": \"test\"}\n      // /posts, /posts/1/1          not match\n      router.GET(\"/posts/:var?\", handler)\n      // /posts/                     {\"var\": \"\"}\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/1/                   {\"var\": \"1/\"}\n      // /posts/1/test/2             {\"var\": \"1/test/2\"}\n      router.GET(\"/posts/*var\", handler)\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 {\"var\": \"test\"}\n      // /posts, /posts/, /posts/1/1 not match\n      router.GET(\"/posts/{var}\", handler)\n      router.GET(\"/posts/{var:string}\", handler)\n      // /posts/                     {\"var\": \"\"}\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 {\"var\": \"test\"}\n      // /posts, /posts/1/1          not match\n      router.GET(\"/posts/{var?}\", handler)\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 not match\n      router.GET(\"/posts/{var:int}\", handler)\n      // /posts/1                    not match\n      // /posts/test                 not match\n      // /posts/.1                   {\"var\": \".1\"}\n      // /posts/1.1                  {\"var\": \"1.1\"}\n      // /posts/1.10                 {\"var\": \"1.10\"}\n      router.GET(\"/posts/{var:float}\", handler)\n      // /posts/1                    {\"var\": \"1\"}\n      // /posts/test                 {\"var\": \"test\"}\n      // /posts/test/1               {\"var\": \"test/1\"}\n      router.GET(\"/posts/{var:path}\", handler)\n    #+end_src\n*** Multi parameters in path\n    #+begin_src go\n      // /posts/1                    not match\n      // /posts/prefixtest           {\"var\": \"test\"}\n      router.GET(\"/posts/prefix:var\", handler)\n      // /posts/prefixtest           {\"var:end\": \"test\"}\n      router.GET(\"/posts/prefix:var:end\", handler)\n      // /posts/test                 not match\n      // /posts/test/1               not match\n      // /posts/test/1/test          {\"var\": \"test/1\"}\n      router.GET(\"/posts/*var/test\", handler)\n      // /posts/test                 not match\n      // /posts/test-123             {\"var\": \"test\", \"var1\": \"123\"}\n      router.GET(\"/posts/{var}-{var1:int}\", handler)\n      // /posts/123-test             {\"var\": \"123\", \"var1\": \"test\"}\n      router.GET(\"/posts/{var:int}-{var1}\", handler)\n      // /posts/123-test             {\"var\": \"123\", \"var1\": \"test\"}\n      // /posts/123/test-test        {\"var\": \"123/test\", \"var1\": \"test\"}\n      router.GET(\"/posts/{var:path}-{var1}\", handler)\n      // /posts/1/1/test             {\"var\": \"1\", \"var1\": \"1\", \"var2\": \"test\"}\n      // /posts/test/1/1/test        {\"var\": \"test/1\", \"var1\": \"1\", \"var2\": \"test\"}\n      // /posts/test/1/1/s/test      not match\n      router.GET(\"/posts/{var:path}/{var1:int}/{var2}\", handler)\n      // /posts/1/1/test             {\"var\": \"1\", \"var1\": \"1\", \"var2\": \"test\"}\n      // /posts/test/1/1/test        {\"var\": \"test\", \"var1\": \"1\", \"var2\": \"1/test\"}\n      // /posts/test/1/1/s/test      {\"var\": \"test\", \"var1\": \"1\", \"var2\": \"1/s/test\"}\n      router.GET(\"/posts/{var:path}/{var1:int}/{var2:path}\", handler)\n    #+end_src\n\n*** Named Route\n    #+begin_src go\n      r := forest.New()\n      g1 := r.Group(forest.WithPrefix(\"/api\"), forest.WithName(\"g1\"))\n      g2 := g1.Group(forest.WithPrefix(\"/v1\"), forest.WithName(\"g2\"))\n      r1 := api.GET(\"/posts\").Named(\"list_posts\", \"some description\")\n      r2 := api.DELETE(\"/posts/:pk\").Named(\"delete_post\", \"delete post with pk param\")\n      // result\n      r.Route(\"g1.g2.list_posts\") == r1\n      r.URL(\"g1.g2.list_posts\") == r1.URL() == \"/v1/api/posts\"\n      r.Route(\"g1.g2.delete_post\") == r2\n      r.URL(\"g1.g2.delete_post\", \"12\") == r2.URL(\"12\") == \"/v1/api/posts/12\"\n    #+end_src\n\n*** Server Static files\n    #+begin_src go\n      r := forest.New()\n      r.GET(\"/static/*\", func(c forest.Context) error {\n          path := filepath.Join(\"static\", c.Param(\"*\"))\n          return c.FileFromFS(path, http.FS(staticFS))\n      })\n      r.GET(\"/robots.txt\", func(c forest.Context) error {\n          return c.FileFromFS(\"static/robots.txt\", http.FS(staticFS))\n      })\n      r.GET(\"/favicon.ico\", func(c forest.Context) error {\n          return c.FileFromFS(\"static/favicon.ico\", http.FS(staticFS))\n      })\n    #+end_src\n\n*** Bind Params\n    #+begin_src go\n      type Params struct {\n          Text string `query:\"text\" json:\"text\" form:\"text\" param:\"text\"`\n      }\n      p := Params{}\n      // bind query, method: not POST, PUT, PATCH\n      // bind form or json or xml, method: POST, PUT, PATCH\n      c.Bind(\u0026p)\n      // bind params, GET /test/:text\n      c.BindParams(\u0026p)\n      // bind other params\n      c.BindWith(\u0026p, bind.Query)\n      c.BindWith(\u0026p, bind.Form)\n      c.BindWith(\u0026p, bind.MultipartForm)\n      c.BindWith(\u0026p, bind.JSON)\n      c.BindWith(\u0026p, bind.XML)\n      c.BindWith(\u0026p, bind.Params)\n      c.BindWith(\u0026p, bind.Header)\n      // custom bind tag\n      c.BindWith(\u0026p, bind.FormBinder{\"json\"})\n      c.BindWith(\u0026p, bind.QueryBinder{\"json\"})\n    #+end_src\n\n** Custom\n*** Custom Middleware\n    #+begin_src go\n      func MyMiddleware(c forest.Context) error {\n          // do something\n          // c.Next() is required, or else your handler will not execute\n          return c.Next()\n      }\n      router := forest.New()\n      // with root\n      router.Use(MyMiddleware)\n      // with group\n      group := router.Group(forest.WithPrefix(\"/api/v1\"), forest.WithMiddlewares(MyMiddleware))\n      // with special handler\n      group.GET(\"/\", MyMiddleware, func(c forest.Context) error {\n          return nil\n      })\n    #+end_src\n\n*** Custom Logger\n    #+begin_src go\n      router := forest.New()\n      router.Logger = Logger1\n\n      router.GET(\"/posts\", func(c forest.Context) error {\n          // c.Logger() == Logger1\n          ...\n          })\n\n      group := router.Group(forest.WithPrefix(\"/api/v1\"))\n      group.GET(\"/posts\", func(c forest.Context) error {\n          // c.Logger() == Logger1\n          ...\n          })\n\n      group := router.Group(forest.WithPrefix(\"/api/v2\"))\n      group.Logger = Logger2\n      group.GET(\"/posts\", func(c forest.Context) error {\n          // c.Logger() == Logger2\n          ...\n          })\n    #+end_src\n\n*** Custom Error Handler\n    #+begin_src go\n      router := forest.New()\n      // engine only\n      router.NotFound(func(c forest.Context) error {\n          return c.JSON(404, forest.H{\"message\": \"not found\"})\n      })\n      router.MethodNotAllowed(func(c forest.Context) error {\n          return c.JSON(405, forest.H{\"message\": \"method not allowed\"})\n      })\n\n      router.ErrorHandler = func(err error, c Context) {\n          c.String(500, err.Error())\n      }\n      group := router.Group(forest.WithPrefix(\"/api/v1\"))\n      // group only\n      group.ErrorHandler = func(err error, c Context) {\n          c.String(501, err.Error())\n      }\n    #+end_src\n\n*** Custom Context\n    #+begin_src go\n      type MyContext struct {\n          forest.Context\n      }\n\n      func (c *MyContext) Next() error {\n          return c.NextWith(c)\n      }\n\n      func MyContextMiddleware(c forest.Context) error {\n          // doing somthing\n          return c.NextWith(\u0026MyContext{c})\n      }\n    #+end_src\n\n*** Custom Host Matcher\n    #+begin_src go\n      func matcher(host, dst string) bool {\n          return host == dst\n      }\n      r := forest.New(forest.HostMatch(matcher))\n      // or use internal matcher\n      r := forest.New(forest.HostMatch(forest.HostMatcher))\n    #+end_src\n\n*** Custom URL Param\n    #+begin_src go\n      import (\n          \"github.com/google/uuid\"\n      )\n\n      type UUIDMatcher struct {\n      }\n\n      func (s *UUIDMatcher) Name() string {\n          return \"uuid\"\n      }\n\n      func (s *UUIDMatcher) Match(path string, index int, next bool) (int, bool) {\n          if index \u003e 0 {\n              return 0, false\n          }\n          if len(path) \u003c 18 || (!next \u0026\u0026 len(path) \u003e 18) {\n              return 0, false\n          }\n          _, err := uuid.Parse(path[:18])\n          if err != nil {\n              return 0, false\n          }\n          return 18, true\n      }\n\n      func NewUUIDMatcher(rule string) forest.Matcher {\n          return \u0026UUIDMatcher{}\n      }\n\n      forest.RegisterRule(\"uuid\", NewUUIDMatcher)\n\n      router := forest.New()\n      router.GET(\"/api/v1/user/{pk:uuid}\", handler)\n    #+end_src\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhonmaple%2Fforest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhonmaple%2Fforest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhonmaple%2Fforest/lists"}