{"id":39600110,"url":"https://github.com/monadicstack/frodo","last_synced_at":"2026-01-18T07:51:55.284Z","repository":{"id":39856054,"uuid":"328401551","full_name":"monadicstack/frodo","owner":"monadicstack","description":"A code generator that turns plain old Go services into RPC-enabled (micro)services with robust HTTP APIs.","archived":false,"fork":false,"pushed_at":"2023-07-03T20:03:37.000Z","size":14161,"stargazers_count":26,"open_issues_count":4,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-01-14T12:27:30.913Z","etag":null,"topics":["api","go","microservices","rest","rpc"],"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/monadicstack.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-01-10T14:24:32.000Z","updated_at":"2024-07-25T15:23:23.000Z","dependencies_parsed_at":"2024-06-19T11:31:30.911Z","dependency_job_id":"17c52682-f299-4d03-ad0e-2b9041f729de","html_url":"https://github.com/monadicstack/frodo","commit_stats":null,"previous_names":["robsignorelli/frodo"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/monadicstack/frodo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monadicstack%2Ffrodo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monadicstack%2Ffrodo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monadicstack%2Ffrodo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monadicstack%2Ffrodo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/monadicstack","download_url":"https://codeload.github.com/monadicstack/frodo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monadicstack%2Ffrodo/sbom","scorecard":{"id":658053,"data":{"date":"2025-08-11","repo":{"name":"github.com/monadicstack/frodo","commit":"9dfa87098f0b0e84c9c80b0c8730ac36b55f4a05"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.5,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":0,"reason":"Found 1/29 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 26 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T15:13:21.852Z","repository_id":39856054,"created_at":"2025-08-21T15:13:21.852Z","updated_at":"2025-08-21T15:13:21.852Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28533231,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T00:39:45.795Z","status":"online","status_checked_at":"2026-01-18T02:00:07.578Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["api","go","microservices","rest","rpc"],"created_at":"2026-01-18T07:51:55.128Z","updated_at":"2026-01-18T07:51:55.227Z","avatar_url":"https://github.com/monadicstack.png","language":"Go","readme":"[![Go Report Card](https://goreportcard.com/badge/github.com/monadicstack/frodo)](https://goreportcard.com/report/github.com/monadicstack/frodo)\n\n# Frodo\n\nFrodo is a code generator and runtime library that helps\nyou write RPC-enabled (micro) services and APIs. It parses\nthe interfaces/structs/comments in your service code to \ngenerate all of your client/server communication code.\n\n* No .proto files. Your services are just idiomatic Go code.\n* Auto-generate APIs that play nicely with `net/http`, middleware, and other standard library compatible API solutions.  \n* Auto-generate RPC-style clients in multiple languages like Go, JavaScript, Dart, etc.\n* Auto-generate strongly-typed mock implementations of your service for unit testing.\n* Create OpenAPI documentation so others know how to interact with your API (if they can't use the client).\n\nFrodo automates all the boilerplate associated with service\ncommunication, data marshaling, routing, error handling, etc. You\nget to focus on writing business logic and features while Frodo gives\nyou all of that other stuff to turn it into a distributed system for free.\nBonus - because Frodo generates clients in multiple languages, your web\nand mobile frontends get to consume your services for free.\n\nTools like gRPC solve similar problems by giving you a complex\nairplane cockpit filled with knobs and dials most of us don't want/need.\nFrodo is the autopilot button that gets most of us where we need to go\nwith as little fuss as possible.\n\n## Table of Contents\n\n* [Getting Started](https://github.com/monadicstack/frodo#getting-started)\n* [Example](https://github.com/monadicstack/frodo#example)\n* [Customize HTTP Route, Status, etc](https://github.com/monadicstack/frodo#doc-options-custom-urls-status-etc)\n* [Error Handling](https://github.com/monadicstack/frodo#error-handling)\n* [Middleware](https://github.com/monadicstack/frodo#middleware)\n* [Returning Raw File Data](https://github.com/monadicstack/frodo#returning-raw-file-data)\n* [HTTP Redirects](https://github.com/monadicstack/frodo#http-redirects)\n* [Request Scoped Metadata](https://github.com/monadicstack/frodo#request-scoped-metadata)\n* [Create a JavaScript Client](https://github.com/monadicstack/frodo#creating-a-javascript-client)\n* [Create a Dart/Flutter Client](https://github.com/monadicstack/frodo#creating-a-dartflutter-client)\n* [Authorization](https://github.com/monadicstack/frodo#authorization)\n* [Handling Not Found](https://github.com/monadicstack/frodo#handling-not-found)\n* [Composing Gateways](https://github.com/monadicstack/frodo#composing-gateways)\n* [Mocking Services](https://github.com/monadicstack/frodo#mocking-services)\n* [Generating OpenAPI Documentation](https://github.com/monadicstack/frodo#generate-openapiswagger-documentation-experimental)\n* [Go Generate Support](https://github.com/monadicstack/frodo#go-generate-support)\n* [Bring Your Own Templates](https://github.com/monadicstack/frodo#bring-your-own-templates)\n* [New Service Scaffolding](https://github.com/monadicstack/frodo#create-a-new-service-w-frodo-create)\n* [Why Not gRPC?](https://github.com/monadicstack/frodo#why-not-just-use-grpc) (motivation for this project)\n\n## Getting Started\n\n*Frodo requires Go 1.16+ as it uses `fs.FS` and `//go:embed` to load templates.*\n\n```shell\ngo install github.com/monadicstack/frodo\n```\nThis will fetch the `frodo` code generation executable as well\nas the runtime libraries that allow your services to\ncommunicate with each other.\n\n\n## Example\n\n#### Step 1: Describe Your Service\n\nYour first step is to write a .go file that just defines\nthe contract for your service; the interface as well as the\ninputs/outputs. \n\n```go\n// calculator_service.go\npackage calc\n\nimport (\n    \"context\"\n)\n\ntype CalculatorService interface {\n    Add(context.Context, *AddRequest) (*AddResponse, error)\n    Sub(context.Context, *SubRequest) (*SubResponse, error)\n}\n\ntype AddRequest struct {\n    A int\n    B int\n}\n\ntype AddResponse struct {\n    Result int\n}\n\ntype SubRequest struct {\n    A int\n    B int\n}\n\ntype SubResponse struct {\n    Result int\n}\n```\nOne important detail is that the interface name ends with\nthe suffix \"Service\". This tells Frodo that this is an\nactual service interface and not just some random abstraction\nin your code.\n\nAt this point you haven't actually defined *how* this service gets\nthis work done; just which operations are available.\n\nWe actually have enough for `frodo` to\ngenerate your RPC/API code already, but we'll hold off\nfor a moment. Frodo frees you up to focus on building\nfeatures, so let's actually implement service; no networking,\nno marshaling, no status stuff, just logic to make your\nservice behave properly.\n\n```go\n// calculator_service_handler.go\npackage calc\n\nimport (\n    \"context\"\n)\n\ntype CalculatorServiceHandler struct {}\n\nfunc (svc CalculatorServiceHandler) Add(ctx context.Context, req *AddRequest) (*AddResponse, error) {\n    result := req.A + req.B\n    return \u0026AddResponse{Result: result}, nil\n}\n\nfunc (svc CalculatorServiceHandler) Sub(ctx context.Context, req *SubRequest) (*SubResponse, error) {\n    result := req.A - req.B\n    return \u0026SubResponse{Result: result}, nil\n}\n```\n\n#### Step 2: Generate Your RPC Client and Gateway\n\nAt this point, you've just written the same code that you (hopefully)\nwould have written even if you weren't using Frodo. Next,\nwe want to auto-generate two things:\n\n* A \"gateway\" that allows an instance of your CalculatorService\n  to listen for incoming requests (via an HTTP API).\n* A \"client\" struct that communicates with that API to get work done.\n\nJust run these two commands in a terminal:\n\n```shell\n# Feed it the service interface code, not the handler.\nfrodo gateway calculator_service.go\nfrodo client  calculator_service.go\n```\n\n#### Step 3: Run Your Calculator API Server\n\nLet's fire up an HTTP server on port 9000 that makes your service\navailable for consumption (you can choose any port you want, obviously).  \n\n```go\npackage main\n\nimport (\n    \"net/http\"\n\n    \"github.com/your/project/calc\"\n    calcrpc \"github.com/your/project/calc/gen\"\n)\n\nfunc main() {\n    service := calc.CalculatorServiceHandler{}\n    gateway := calcrpc.NewCalculatorServiceGateway(service)\n    http.ListenAndServe(\":9000\", gateway)\n}\n```\nSeriously. That's the whole program.\n\nCompile and run it, and your service/API is now ready\nto be consumed. We'll use the Go client we generated in just\na moment, but you can try this out right now by simply\nusing curl:\n\n```shell\ncurl -d '{\"A\":5, \"B\":2}' http://localhost:9000/CalculatorService.Add\n# {\"Result\":7}\ncurl -d '{\"A\":5, \"B\":2}' http://localhost:9000/CalculatorService.Sub\n# {\"Result\":3}\n```\n\n#### Step 4: Consume Your Calculator Service\n\nWhile you can use raw HTTP to communicate with the service,\nlet's use our auto-generated client to hide the gory\ndetails of JSON marshaling, status code translation, and\nother noise.\n\nThe client actually implements CalculatorService\njust like the server/handler does. As a result the RPC-style\ncall will \"feel\" like you're executing the service work\nlocally, when in reality the client is actually making API\ncalls to the server running on port 9000.\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/your/project/calc\"\n    \"github.com/your/project/calc/gen\"\n)\n\nfunc main() {\n    ctx := context.Background()\n    client := calcrpc.NewCalculatorServiceClient(\"http://localhost:9000\")\n\n    add, err := client.Add(ctx, \u0026calc.AddRequest{A:5, B:2})\n    if err != nil {\n        log.Fatalf(err.Error())\n    }\n    fmt.Println(\"5 + 2 = \", add.Result)\n\n    sub, err := client.Sub(ctx, \u0026calc.SubRequest{A:5, B:2})\n    if err != nil {\n        log.Fatalf(err.Error())\n    }\n    fmt.Println(\"5 - 2 = \", sub.Result)\n}\n```\n\nCompile/run this program, and you should see the following output:\n\n```\n5 + 2 = 7\n5 - 2 = 3\n```\nThat's it!\n\nFor more examples of how to write services that let Frodo take\ncare of the RPC/API boilerplate, take a look in the [example/](https://github.com/monadicstack/frodo/tree/main/example)\ndirectory of this repo.\n\n## Doc Options: Custom URLs, Status, etc\n\nFrodo gives you a remote service/API that \"just works\" out of the\nbox. You can, however customize the API routes for individual operations,\nset a prefix for all routes in a service, and more using \"Doc Options\"...\nworst Spider-Man villain ever.\n\nHere's an example with all the available options. They are all\nindependent, so you can specify a custom status without specifying\na custom route and so on.\n\n```go\n// CalculatorService provides some basic arithmetic operations.\n//\n// VERSION 0.1.3\n// PATH /v1\ntype CalculatorService interface {\n    // Add calculates the sum of A + B.\n    //\n    // HTTP 202\n    // GET /addition/:A/:B\n    Add(context.Context, *AddRequest) (*AddResponse, error)\n\n    // Sub calculates the difference of A - B.\n    //\n    // GET /subtraction/:A/:B\n    Sub(context.Context, *SubRequest) (*SubResponse, error)\n}\n```\n\n#### Service: PATH\n\nThis prepends your custom value on every route in the API. It applies\nto the standard `ServiceName.FunctionName` routes as well as custom routes\nas we'll cover in a moment. \n\nYour generated API and RPC clients will be auto-wired to use the prefix \"v1\" under the\nhood, so you don't need to change your code any further. If you want\nto hit the raw HTTP endpoints, however, here's how they look now:\n\n```shell\ncurl -d '{\"A\":5, \"B\":2}' http://localhost:9000/v1/CalculatorService.Add\n# {\"Result\":7}\n\ncurl -d '{\"A\":5, \"B\":2}' http://localhost:9000/v1/CalculatorService.Sub\n# {\"Result\":3}\n```\n\n#### Function: GET/POST/PUT/PATCH/DELETE\n\nYou can replace the default `POST ServiceName.FunctionName` route for any\nservice operation with the route of your choice. In the example, the path parameters `:A` and `:B`\nwill be bound to the equivalent A and B attributes on the request struct.\n\nHere are the updated curl calls after we generate the new\ngateway code. Notice it's also taking into account the service's PATH\nprefix as well:\n\n```shell\ncurl http://localhost:9000/v1/addition/5/2\n# {\"Result\":7}\ncurl http://localhost:9000/v1/subtraction/5/2\n# {\"Result\":3}\n```\n\n#### Function: HTTP\n\nThis lets you have the API return a non-200 status code on success.\nFor instance, the Add function's route will return a \"202 Accepted\"\nstatus when it responds with the answer instead of \"200 OK\".\n\n## Error Handling\n\nBy default, if your service call returns a non-nil error, the\nresulting RPC/HTTP request will have a 500 status code. You\ncan, however, customize that status code to correspond to the type\nof failure (e.g. 404 when something was not found).\n\nThe easiest way to do this is to just use the `rpc/errors`\npackage when you encounter a failure case:\n\n```go\nimport (\n    \"github.com/monadicstack/frodo/rpc/errors\"\n)\n\nfunc (svc UserService) Get(ctx context.Context, req *GetRequest) (*GetResponse, error) {\n    if req.ID == \"\" {\n        return nil, errors.BadRequest(\"id is required\")\n    }\n    user, err := svc.Repo.GetByID(req.ID)\n    if err != nil {\n    \treturn nil, err\n    }\n    if user == nil {\n        return nil, errors.NotFound(\"user not found: %s\", req.ID)\n    }\n    return \u0026GetResponse{User: user}, nil\n}\n```\n\nIn this case, the caller will receive an HTTP 400 if they\ndidn't provide an id, a 404 if there is no user with that\nid, and a 500 if any other type of error occurs.\n\nWhile the error categories in Frodo's errors package is\nprobably good enough for most people, take a look at the\ndocumentation for [github.com/monadicstack/respond](https://github.com/monadicstack/respond#how-does-it-know-which-4xx5xx-status-to-use)\nto see how you can roll your own custom errors, but still\ndrive which 4XX/5XX status your service generates.\n\n## Middleware\n\nYour RPC gateway is just an `http.Handler`, so you can plug\nand play your favorite off-the-shelf middleware. Here's an\nexample using [github.com/urfave/negroni](https://github.com/urfave/negroni)\n\n```go\nfunc main() {\n    service := calc.CalculatorServiceHandler{}\n    gateway := calcrpc.NewCalculatorServiceGateway(service,\n        rpc.WithMiddleware(\n            negroni.NewLogger().ServeHTTP,\n            NotOnMonday,\n        ))\n\n    http.ListenAndServe(\":9000\", gateway)\n}\n\nfunc NotOnMonday(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {\n    if time.Now().Weekday() == time.Monday {\n        http.Error(w, \"garfield says no math on mondays\", 403)\n        return\n    }\n    next(w, req)\n}\n```\n\nYou might think to yourself... wait a minute; I thought the gateway\n*was* an HTTP handler, so couldn't I just wrap the gateway in middleware\nlike this?\n\n```go\ngateway := calcrpc.NewCalculatorServiceGateway(service)\nhandler := negroni.New(\n    negroni.NewLogger(),\n    negroni.WrapFunc(NotOnMonday),\n)\nhandler.UseHandler(gateway)\nhttp.ListenAndServe(\":9000\", handler)\n```\n\nYou absolutely can, and it will work great... mostly. Frodo's gateway\nperforms a few book-keeping tasks before it executes your\nmiddleware and the eventual service function. One of these tasks is\nrestoring request-scoped metadata headers passed from the caller (next section).\nIf you don't actually need that information, then this works. If you want\nthe full arsenal of Frodo functionality in your middleware functions,\nbe sure to use `.WithMiddleware()` like in the first example.\n\n## Returning Raw File Data\n\nLet's say that you're writing `ProfilePictureService`. One of the\noperations you might want is the ability to return the raw JPG data\nfor a user's profile picture. You do this the same way that you \nhandle JSON-based responses; just implement some interfaces so that\nFrodo knows to treat it a little different:\n\n```go\ntype ServeResponse struct {\n    file *io.File\n}\n\n// By implementing ContentReader, the response tells Frodo to respond\n// w/ raw data rather than JSON. Instead of turning the struct into\n// JSON, grab bytes from your reader and deliver them in the response.\nfunc (res ServeResponse) Content() io.ReadCloser {\n    return res.file\n}\n\n// By implementing ContentTypeReader, this lets you dictate the\n// underlying HTTP Content-Type header. Without this Frodo will have\n// nothing to go on and assume \"application/octet-stream\".\nfunc (res ServeResponse) ContentType() string {\n    return \"image/jpeg\"\n}\n\n// --- and now in your service ---\n\nfunc (svc ProfilePictureService) Serve(ctx context.Context, req *ServeRequest) (*ServeResponse, error) {\n    // Ignore the fact that you probably don't store profile pictures on the\n    // hard drive of your service boxes...\n    f, err := os.Open(\"./pictures/\" + req.UserID + \".jpg\")\n    if err != nil {\n        return nil, errors.NotFound(\"no profile picture for user %s\", req.UserID)\n    }\n    return \u0026ServeResponse{file: f}, nil\n}\n```\n\nSince `ServeResponse` implements `io.Reader`, the raw JPG bytes will\nbe sent to the caller instead of the JSON-marshaled version of the\nresult. Also, since it implements the `ContentType()` function, the\ncaller will see it as an \"image/jpg\" rather than \"application/octet-stream\".\n\n## HTTP Redirects\n\nIt's fairly common to have a service call that does some work\nto locate a resource, authorize it, and then redirect to\nS3, CloudFront, or some other CDN to actually serve up\nthe raw asset.\n\nIn Frodo, it's pretty simple. If your XxxResponse struct implements\nthe `respond.Redirector` interface from [github.com/monadicstack/respond](https://github.com/monadicstack/respond)\nthen the gateway will respond with a 307-style redirect\nto the URL of your choice:\n\n```go\n// In video_service.go, this implements the Redirector interface.\ntype DownloadResponse struct {\n    Bucket string\n    Key    string\t\n}\n\nfunc (res DownloadResponse) Redirect() string {\n    return fmt.Sprintf(\"https://%s.s3.amazonaws.com/%s\",\n        res.Bucket,\n        res.Key)\n}\n\n\n// In video_service_handler.go, this will result in a 307-style\n// redirect to the URL returned by \"response.Redirect()\"\nfunc (svc VideoServiceHandler) Download(ctx context.Context, req *DownloadRequest) (*DownloadResponse, error) {\n    file := svc.Repo.Get(req.FileID)\n    return \u0026DownloadResponse{\n        Bucket: file.Bucket,\n        Key:    file.Key, \n    }, nil\n}\n```\n\n## Request Scoped Metadata\n\nWhen you make an RPC call from Service A to Service B, none\nof the values stored on the `context.Context` will be\navailable to you when are in Service B's handler. There are\ninstances, however, where it's useful to have values follow\nevery hop from service to service; request ids, tracing info, etc.\n\nFrodo places a special bag of values called \"metadata\" onto\nthe context which **will** follow you as you go from service\nto service:\n\n```go\nfunc (a ServiceA) Foo(ctx context.Context, r *FooRequest) (*FooResponse, error) {\n    // \"Hello\" will NOT follow you when you call Bar(),\n    // but \"DontPanic\" will. Notice that the metadata\n    // value does not need to be a string like in gRPC.\n    ctx = context.WithValue(ctx, \"Hello\", \"World\")\n    ctx = metadata.WithValue(ctx, \"DontPanic\", 42)\n\n    serviceB.Bar(ctx, \u0026BarRequest{})\n}\n\nfunc (b ServiceB) Bar(ctx context.Context, r *BarRequest) (*BarResponse, error) {\n    a, okA := ctx.Value(\"A\").(string)\n\n    b := 0\n    okB = metadata.Value(ctx, \"DontPanic\", \u0026b)\n    \n    // At this point:\n    // a == \"\"   okA == false\n    // b == 42   okB == true\n}\n```\n\nIf you're wondering why `metadata.Value()` looks more like\n`json.Unarmsahl()` than `context.Value()`, it has to\ndo with a limitation of reflection in Go. When the values\nare sent over the network from Service A to Service B, we\nlose all type information. We need the type info `\u0026b` gives\nus in order to properly restore the original value, so Frodo\nfollows the idiom established by many\nof the decoders in the standard library.\n\n## Creating a JavaScript Client\n\nThe `frodo` tool can actually generate a JS client that you\ncan add to your frontend code (or React Native mobile code)\nto hide the complexity of making API calls to your backend\nservice. Without any plugins or fuss, we can create a JS client of the same\nCalculatorService from earlier...\n\n```shell\nfrodo client calc/calculator_service.go --language=js\n```\n\nThis will create the file `calculator_service.gen.client.js`\nwhich you can include with your frontend codebase. Using it\nshould look similar to the Go client we saw earlier:\n\n```js\nimport {CalculatorService} from 'lib/calculator_service.gen.client';\n\n// The service client is a class that exposes all of the\n// operations as 'async' functions that resolve with the\n// result of the service call.\nconst service = new CalculatorService('http://localhost:9000');\nconst add = await service.Add({A:5, B:2});\nconst sub = await service.Sub({A:5, B:2});\n\n// Should print:\n// Add(5, 2) = 7\n// Sub(5, 2) = 3\nconsole.info('Add(5, 2) = ' + add.Result)\nconsole.info('Sub(5, 2) = ' + sub.Result)\n```\n\nAnother subtle benefit of using Frodo's client is that all of your\nservice/function documentation follows you in the generated code.\nIt's included in the JSDoc of the client so all of your service/API\ndocumentation should be available to your IDE even when writing\nyour frontend code.\n\n#### Node Support\n\nYou can actually use this client in your Node\nserver-side code as well to call service functions in your Go API.\nThe client uses the 'fetch' API to handle the HTTP layer. In the browser,\nit will just use the window-scoped 'fetch' instance, but you can supply\nyour own to the constructor of your service client:\n\n```js\nconst fetch = require('node-fetch');\n\n// Just inject your 'fetch' implementation to the construtor and everything\n// should work exactly the same.\nconst service = new CalculatorService('http://localhost:9000', {fetch});\nconst add = await service.Add({A:5, B:2});\nconst sub = await service.Sub({A:5, B:2});\n```\n\n## Creating a Dart/Flutter Client\n\nJust like the JS client, Frodo can create a Dart client that you can embed\nin your Flutter apps so mobile frontends can consume your service.\n\n```shell\nfrodo client calc/calculator_service.go --language=dart\n  or\nfrodo client calc/calculator_service.go --language=flutter\n```\n\nThis will create the file `calculator_service.gen.client.dart`. Add it\nto your Flutter codebase and it behaves very similarly to the JS client.\n\n```dart\nimport 'lib/calculator_service.gen.client.dart';\n\nvar service = CalculatorServiceClient(\"http://localhost:9000\");\nvar add = await service.Add(A:5, B:2);\nvar sub = await service.Sub(A:5, B:2);\n\n// Should print:\n// Add(5, 2) = 7\n// Sub(5, 2) = 3\nprint('Add(5, 2) = ${add.Result}');\nprint('Sub(5, 2) = ${sub.Result}');\n```\n\n## Authorization\n\nSince you probably want your services to do some sort of authentication\nand authorization, Frodo helps you manage the HTTP `Authorization` header.\nWhen the gateway accepts a request, it stores the header in a context value\nthat you can fetch in middleware or your handler, so you can decide how\nto utilize that info.\n\n```go\nimport (\n    \"github.com/monadicstack/frodo/rpc/authorization\"\n    \"github.com/monadicstack/frodo/rpc/errors\"\n)\n\nfunc (a *ServiceA) Hello(ctx contex.Context, req *HelloRequest) (*HelloResponse, error) {\n    auth := authorization.FromContext(ctx)\n    if auth.Empty() {\n        return nil, errors.BadCredentials(\"missing authorization header\")\n    }\n    if auth.String() == \"Donny\" {\n        return nil, errors.PermissionDenied(\"you're out of your element\")\t\n    }\n    return \u0026HelloResponse{Text: \"Hello\"+req.Name}, nil\n}\n```\nIgnore the awful security of hardcoding valid/invalid credentials;\nthe value for `auth` should be the value\nof the `Authorization` header on the incoming HTTP request. The whole\nidea is that your business logic exists independent of HTTP-related stuff,\nso Frodo takes that HTTP-provided data and puts it on the context. This\nallows your handler to deal with credentials in a transport-independent fashion.\n\n\nFrodo, however, just makes sure that you have the value that was given. With that\nvalue in hand, you can feed that to your favorite OAuth2, JWT, or whatever\nlibrary/middleware to do something meaningful with it. \n\n### Supplying Authorization Credentials\n\nIn the previous example we assumed that the caller supplied authorization\ncredentials and just retrieved/used them. Since authorization is just a value stored\non the context, you can supply them fairly easily:\n\n```go\n// Supply \"Authorization: Token 12345\" when calling the Hello endpoint\nauth := authorization.New(\"Token 12345\") \nctx = authorization.WithHeader(ctx, auth)\nclientA.Hello(ctx, \u0026servicea.HelloRequest{Name: \"Bob\"})\n```\n\nThis works when you are making the initial call, but to make life easier,\nFrodo will also include that authorization on every other RPC-driven service call you make in that\nrequest scope:\n\n\n```go\nfunc (a *ServiceA) Hello(ctx contex.Context, req *HelloRequest) (*HelloResponse, error) {\n    fmt.Printf(\"Authorization A: %v\\n\", authorization.FromContext(ctx))\n    clientB.EatTacos(ctx, \u0026serviceb.EatTacosRequest{})\n    ...\n}\n\nfunc (b *ServiceB) EatTacos(ctx contex.Context, req *EatTacosRequest) (*EatTacosResponse, error) {\n    fmt.Printf(\"Authorization B: %v\\n\", authorization.FromContext(ctx))\n    clientC.Goodbye(ctx, \u0026serviceb.EatTacosRequest{})\n    ...\n}\n\nfunc (c *ServiceC) Goodbye(ctx contex.Context, req *GoodbyeRequest) (*GoodbyeResponse, error) {\n    fmt.Printf(\"Authorization C: %v\\n\", authorization.FromContext(ctx))\n    ...\n}\n\n// Output\n// Authorization A: Token 12345\n// Authorization B: Token 12345\n// Authorization C: Token 12345\n```\n\nAgain, ignore the poor architecture - if you have this many dependent\ncalls, you probably need some asynchronous pub/sub in your life. The\npoint of the example, however, is that the credentials \"Token 12345\" was\nonly explicitly provided to the initial call to `Hello()`, but it was automatically propagated to service B and C because\nyou threaded the context through the whole thing.\n\n### Authorization Using the JavaScript Client\n\nWhen making the original call to `ServiceA.Hello()`, the JS client\ndoesn't utilize a \"context\" like Go does. To stay idiomatic to JS,\nyou supply it via an options argument when making your\nservice call:\n\n```js\nconst client = new ServiceAClient('...');\nconst req = { Name: 'Bob' };\nclient.Hello(req, { authorization: 'Token 12345' });\n```\n\n### Authorization Using the Dart/Flutter Client\n\nThe pattern is similar to the JS client above. Since we don't have a Go\ncontext equivalent, it's an optional named argument to each of the service methods.\n\n```dart\nvar client = ServiceAClient('...');\nvar req = HelloRequest(Name: 'Bob');\nclient.Hello(req, authorization: 'Token 12345');\n```\n\n## Handling Not Found\n\nAt the end of the day your service is just a series of HTTP\nroutes/endpoints. Even though Frodo will make sure your clients\nalways hit real endpoints, it's still possible for requests\nfor things that don't exist to hit your server. By default,\nFrodo will respond with a basic 404 for routes that do not\nexist in your service and a 405 for routes that do exist\nbut have the wrong method (e.g. \"GET /login\" instead of \"POST /login\").\nFor simplicity Frodo's not found handler takes care of both\ncases.\n\nFrodo provides a functional option for your gateway to let you\nappend to or completely change this behavior. Just like you\ncan provide middleware functions for \"hits\" to valid service\nendpoints, you can provide a different set of middleware handlers\nfor requests that don't match your routing table.\n\n```go\n// Create some middleware functions... remember, these\n// all match the function signature:\n//\n//   func(http.ResponseWriter, *http.Request, next http.HandlerFunc)\n//\nlogger := NewLoggerMiddleware()\nmetrics := NewMetricsMiddleware()\nauthenticate := NewAuthenticateMiddleware()\nadminOnly := NewAuthorizeMiddleware(\"admin\")\n\nservice := UserServiceHandler{}\ngateway := usersrpc.NewUserServiceGateway(service,\n    rpc.WithMiddleware(\n        logger,\n        metrics,\n        authenticate,\n        adminOnly,\n    ),\n    rpc.WithNotFoundMiddleware(\n        logger,\n        metrics,\n    ),\n)\n```\n\nIn this case, we'll log and track metrics for your valid\nservice calls as well as make sure to provide some basic\nsecurity around them. For calls to non-existent routes, we\nwill only log/track them and rely on the default response\nhandling for those failures (returning 404 or 405 as needed).\n\nIf, however, you want to insert your own handling and respond\nhow you want, you can treat it just like any other middleware\nfunction and just not call `next()`.\n\n```go\nfunc crashOverride(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {\n    // Don't consider this \"not found\"\n    if req.RequestURI == \"/hack/the/planet\" {\n\t\trespond.To(w, req).Ok(`{\"Cool\":0}`)\n        return\n    }\n\n    // Everything else, give a normal 404/405\n    next(w, req)\n}\n\n// ...\n\nrpc.WithNotFoundMiddleware(\n    logger,\n    metrics,\n    crashOverride,\n),\n```\n\nIn case you're curious, the response handling is done using\nthe https://github.com/monadicstack/respond library. Frodo\nuses it under the hood and can make your life easier, too.\n\n## Composing Gateways\n\nThe default behavior for your service gateways is that they will each\nrun in their own HTTP server, likely in their own processes. There are\na few instances, however, where you might decide that you want to run\nmultiple services in the same server/process. The most common is probably\nfor local development. It can be a pain to start/stop 15 different processes\nfor each of your services, so maybe you just want everything to run\nin a single one. This way when you make a change to any service you just\ndown/up one process and see your changes.\n\nYou can use the `rpc.Compose()` function to take any N service gateways\nand create a single gateway that serves up all of those services/operations.\n\n```go\n// Create gateways for each service like you normally would.\nuserGateway := users.NewUserServiceGateway(userService)\ngroupGateway := groups.NewGroupServiceGateway(groupService)\nprojectGateway := projects.NewProjectServiceGateway(projectService)\n\n// Wrap them in a composed gateway that routes requests to all three.\ngateway := rpc.Compose(\n    userGateway.Gateway,\n    groupGateway.Gateway,\n    projectGateway.Gateway,\n)\nhttp.listenAndService(\":8080\", gateway)\n```\nAll 3 services will be listening on port 8080, so\nyou can access them via their Frodo clients; just give them all the\nsame address:\n\n```go\nuserClient := users.NewUserServiceClient(\"http://localhost:8080\")\ngroupClient := groups.NewGroupServiceClient(\"http://localhost:8080\")\nprojectClient := projects.NewProjectServiceClient(\"http://localhost:8080\")\n```\n\nIf you plan to just hit the API endpoints directly, the\nbase address is the same, but the request paths should still correspond\nto the original gateways:\n\n```\ncurl -d '{\"ID\":\"123\"}' http://localhost:8080/UserService.GetByID\ncurl -d '{\"Name\":\"Foo\"}' http://localhost:8080/GroupService.CreateGroup\ncurl -d '{\"Flag\":true}' http://localhost:8080/ProjectService.ArchiveProject\n```\n\n## Mocking Services\n\nWhen you write tests that rely on your services, Frodo can generate mock instances of your\nthem so that you can customize their behaviors:\n\n```shell\n$ frodo mock calculator_service.go\n```\n\nNow, you can do the following in your tests:\n\n```go\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"github.com/example/calc\"\n    mocks \"github.com/example/calc/gen\"\n)\n\nfunc TestSomethingThatDependsOnAddFailure(t *testing.T) {\n    // You can program behaviors for Add(). If the test code calls Sub()\n    // it will panic since you didn't define a behavior for that operation.\n    svc := mocks.MockCalculatorService{\n        AddFunc: func(ctx context.Context, req *calc.AddRequest) (*calc.AddResponse, error) {\n            return nil, fmt.Errorf(\"barf...\")\n        },\t\n    }\n\n    // Feed your mock service to the thing you're testing\n    something := NewSomething(svc)\n    _, err := something.BlahBlah(100)\n    assertError(err)\n    ...\n\n    // You can also verify invocations on your service:\n    assertEquals(0, svc.Calls.Sub.Times)\n    assertEquals(5, svc.Calls.Add.Times)\n    assertEquals(1, svc.Calls.Add.TimesFor(calc.Request{A: 4, B: 2}))\n    assertEquals(2, svc.Calls.Add.TimesMatching(func(r calc.Request) bool {\n        return r.A \u003e 2\n    }))\n}\n```\n\nFrodo's mocks are not as fully featured as other Go mocking frameworks\nout there, but it's good enough for most standard use cases. Your\nservices are just interfaces, so it's easy enough to bring your own\nmocking framework if this won't work for you.\n\n## Generate OpenAPI/Swagger Documentation (Experimental)\n\nDefinitely a work in progress, but in addition to generating\nyour backend and frontend assets, Frodo can generate OpenAPI 3.0 YAML\nfiles to describe your API. It uses the name/type information from\nyour Go code as well as the GoDoc comments that you (hopefully)\nwrite. Document your code in Go and you can get online API docs for free:\n\n```shell\n$ frodo client calculator_service.go --language=openapi\n  # or\n$ frodo client calculator_service.go --language=swagger\n```\n\nNow you can feed the file `gen/calculator_service.gen.swagger.yaml`\nto your favorite Swagger tools. You can try it out by just pasting\nthe output on https://editor.swagger.io.\n\nOpenAPI docs let you specify the current version of your\nservice. You can specify that value by including the VERSION\ndoc option on your service interface.\n\n```go\n// FooService is a magical service that does awesome things.\n//\n// VERSION 1.2.1\ntype FooService interface {\n    // ...\n}\n```\nNow, when you generate your docs the version badge will display \"1.2.1\".\n\nNot gonna lie... this whole feature is still a work in progress. I've still\ngot some issues to work out with nested request/response structs.\nIt spits out enough good stuff that it should describe your services\nbetter than no documentation at all, though.\n\n## Go Generate Support\n\nIf you prefer to stick to the standard Go toolchain for generating\ncode, you can use `//go:generate` comments to hook Frodo code\ngeneration into your build process. Here's how you can set up your\nservice to generate the gateway, mock service, Go client, and JS client. \n\n```go\nimport (\n   ...\n)\n\n//go:generate frodo gateway $GOFILE\n//go:generate frodo client  $GOFILE\n//go:generate frodo client  $GOFILE --language=js\n//go:generate frodo mock    $GOFILE\n\ntype CalculatorService interface {\n    ...\n}\n```\n\nNow when you want to re-create your RPC artifacts, you can run a\nsingle command (assuming you're already in the 'calc' directory):\n\n```shell\n$ go generate .\n```\n\nYou can also generate artifacts for all of\nyour services at once by using the recursive version from the root of your project:\n\n```shell\n$ go generate ./...\n```\n\n## Bring Your Own Templates\n\nAs Frodo matures, we will try to maintain a large number of templates for\nclients in multiple popular languages (feel free to submit a PR if your\nlanguage of choice is not currently supported). If you have more specialized\nneeds, you can actually bring your own custom code templates to all CLI code\ngeneration sub-commands (client, gateway, mock, and docs).\n\nFor instance, if you have your own JavaScript client template that meets your\nneeds better than the one that ships with Frodo, you can do the following:\n\n```shell\nfrodo client calculator_service.go \\\n  --language=js \\\n  --template=mytemplates/myclient.js.tmpl\n```\n\nThe path to the template can be either relative to where you're running the\ncommand or an absolute path to a template on your hard drive. Either way, just\nmake sure that your template expects the root value to be a Frodo `*parser.Context`.\n\n## Create a New Service w/ `frodo create`\n\nThis is 100% optional. As we saw in the initial example,\nyou can write all of your Go code starting with empty\nfiles and have a fully distributed service in a few lines of code.\n\nThe `frodo` tool, however, has a command that generates a\nlot of that boilerplate for you so that you can get straight\nto solving your customers' problems.\n\nLet's assume that you want to make a new service called\n`UserService`, you can execute any  of the following commands:\n\n```shell\nfrodo create user\n  # or\nfrodo create User\n  # or\nfrodo create UserService\n```\n\nThis will create a new package in your project with all of the\nfollowing assets created:\n\n```\n[project]\n  user/\n    makefile\n    user_service.go\n    user_service_handler.go\n    cmd/\n      main.go\n    gen/\n      user_service.gen.gateway.go\n      user_service.gen.client.go\n```\n\nThe service will have a dummy `Create()` function\njust so that there's *something* defined. You should replace\nthat with your own functions and implement them to make the service\ndo something useful.\n\nThe makefile has some convenience targets for building/running/testing\nyour new service as you make updates. The `build` target even\nmakes sure that your latest service updates get re-frodo'd so\nyour gateway/client are always in sync.\n\n## Why Not Just Use gRPC?\n\nSimply put... complexity. gRPC and grpc-gateway solve a lot of hard problems\nrelated to distributed systems at massive scale, but those solutions come at\nthe cost of simplicity. Countless hours have been lost debugging issues\nresolving missing dependencies in proto files, or trying to get a load balancing\nsolution to work, or figuring out whether to write logic in HTTP middleware or\na gRPC interceptor (just to name a few common pain points).\n\nIf many of us are honest with ourselves, a lot of\nwhat gRPC and its ecosystem offers falls into the [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it)\nrealm (you ain't gonna need it).\n\nSimple solutions that create JSON-based HTTP APIs are good enough\nfor most of us. In those cases gRPC tends to introduce a lot of complexity without\ngiving as much in return. Frodo strives to have a much better developer\nexperience that takes you from your first line of code to a solid\nset of services/APIs that are easy to maintain, test, deploy, and scale.\n\nFor the vast majority of projects, JSON (un)marshaling is probably not a performance bottleneck for you. Protobufs\nare a great solution for a very specific problem, but it's one that\nvery, very few applications actually have. I've seen REST APIs handle hundreds\nof millions of requests per day using Go's `encoding/json` package. While\ncool tech, protobufs just do not solve a problem that most of us have, so\nwe might was well avoid the complexity that comes along with them.\n\nEven if you get things working on your development machine, gRPC introduces\na series of other problems that you're left having to solve such as\nload balancing. You could do client-side load balancing, but that involves\nyou having to also figure out etcd, Consul, or some other service\ndiscovery solution. You could do server-side load balancing, but\nyou're going to need something like Linkerd, Envoy, or some other\nnot-so-simple service mesh solution. Frodo's services work the same as any other\nAPI you might build by hand with the standard library, Gin, Chi, Echo, etc., so\nthe load balancers provided by AWS/GCP/Azure\njust work.\n\nUltimately, Frodo tries to take as much the \"good\" from gRPC and its\necosystem while eliminating as much of the \"bad\" as possible. Frodo\nfocuses more on the developer experience and less on giving you options\ngalore. It tries to provide simple solutions and sane defaults for common\nproblems so you spend less time figuring out how to make Frodo work and\nmore time solving your users' problems.\n\nHere are a few conscious deviations from how gRPC does things:\n\n* No proto files or other quirky DSLs to learn. Just write a Go interfaces/structs\n  and Frodo will figure out automatically.\n* Setup is as easy as one `go install` to get every feature. gRPC requires you to manually install protoc\n  then fetch 3 or 4 grpc-related/Go dependencies, and not all have properly\n  adopted Go modules yet.\n* The CLI is much less complex. Even a simple gRPC service\n  with an API gateway requires around 9 or 10 arguments to `protoc` in order\n  to function. Contrast that with `frodo gateway foo/service.go`.\n* If you don't like the CLI, you can hook into `go:generate` instead.\n* In gRPC, the client it generates does not implement the service interface. It's\n  really close but not enough for a strongly typed language like Go. Frodo\n  makes it so that clients/gateways both implement your service interface. This\n  gives you more flexibility to swap interacting with a local vs remote\n  instance of the service w/ no code changes.\n* The RPC layer is just JSON over HTTP. Your frontend can consume\n  your services the exact same way that other backend services do.\n* You've got an entire ecosystem of off-the-shelf solutions for middleware\n  for logging, security, etc regardless of\n  where the request comes from. With gRPC, the rules are\n  different if the request came from another service vs\n  through your API gateway (e.g. from your frontend).\n* In gRPC, you have to jump through some [crazy hoops](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/) if you \n  want anything other than a status of 200 for success 500 for failure.\n  With Frodo, you can use idiomatic errors and one-line changes\n  to customize this behavior.\n* Since Frodo uses standard-library HTTP, traditional load balancing\n  solutions like nginx or your cloud provider's load balancer\n  gets the job done. No need to introduce etcd, Consul, Linkerd, Envoy,\n  or any other technology into your architecture.\n* Better metadata. Request-scoped metadata in gRPC is basically a map\n  of string values. This forces you to marshal/unmarshal other types yourself.\n  Frodo's metadata lets you pass around any type of data you want as\n  you hop from service to service and will handle all that noise for you.\n* Frodo has a stronger focus on generated code that is actually\n  readable. If you want to treat Frodo RPC like a black box,\n  you can. If you want to peek under the hood, however, you can\n  do so with only minimal tears, hopefully.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonadicstack%2Ffrodo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmonadicstack%2Ffrodo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonadicstack%2Ffrodo/lists"}