Frodo
A code generator that turns plain old Go services into RPC-enabled (micro)services with robust HTTP APIs.
Install / Use
/learn @monadicstack/FrodoREADME
Frodo
Frodo is a code generator and runtime library that helps you write RPC-enabled (micro) services and APIs. It parses the interfaces/structs/comments in your service code to generate all of your client/server communication code.
- No .proto files. Your services are just idiomatic Go code.
- Auto-generate APIs that play nicely with
net/http, middleware, and other standard library compatible API solutions. - Auto-generate RPC-style clients in multiple languages like Go, JavaScript, Dart, etc.
- Auto-generate strongly-typed mock implementations of your service for unit testing.
- Create OpenAPI documentation so others know how to interact with your API (if they can't use the client).
Frodo automates all the boilerplate associated with service communication, data marshaling, routing, error handling, etc. You get to focus on writing business logic and features while Frodo gives you all of that other stuff to turn it into a distributed system for free. Bonus - because Frodo generates clients in multiple languages, your web and mobile frontends get to consume your services for free.
Tools like gRPC solve similar problems by giving you a complex airplane cockpit filled with knobs and dials most of us don't want/need. Frodo is the autopilot button that gets most of us where we need to go with as little fuss as possible.
Table of Contents
- Getting Started
- Example
- Customize HTTP Route, Status, etc
- Error Handling
- Middleware
- Returning Raw File Data
- HTTP Redirects
- Request Scoped Metadata
- Create a JavaScript Client
- Create a Dart/Flutter Client
- Authorization
- Handling Not Found
- Composing Gateways
- Mocking Services
- Generating OpenAPI Documentation
- Go Generate Support
- Bring Your Own Templates
- New Service Scaffolding
- Why Not gRPC? (motivation for this project)
Getting Started
Frodo requires Go 1.16+ as it uses fs.FS and //go:embed to load templates.
go install github.com/monadicstack/frodo
This will fetch the frodo code generation executable as well
as the runtime libraries that allow your services to
communicate with each other.
Example
Step 1: Describe Your Service
Your first step is to write a .go file that just defines the contract for your service; the interface as well as the inputs/outputs.
// calculator_service.go
package calc
import (
"context"
)
type CalculatorService interface {
Add(context.Context, *AddRequest) (*AddResponse, error)
Sub(context.Context, *SubRequest) (*SubResponse, error)
}
type AddRequest struct {
A int
B int
}
type AddResponse struct {
Result int
}
type SubRequest struct {
A int
B int
}
type SubResponse struct {
Result int
}
One important detail is that the interface name ends with the suffix "Service". This tells Frodo that this is an actual service interface and not just some random abstraction in your code.
At this point you haven't actually defined how this service gets this work done; just which operations are available.
We actually have enough for frodo to
generate your RPC/API code already, but we'll hold off
for a moment. Frodo frees you up to focus on building
features, so let's actually implement service; no networking,
no marshaling, no status stuff, just logic to make your
service behave properly.
// calculator_service_handler.go
package calc
import (
"context"
)
type CalculatorServiceHandler struct {}
func (svc CalculatorServiceHandler) Add(ctx context.Context, req *AddRequest) (*AddResponse, error) {
result := req.A + req.B
return &AddResponse{Result: result}, nil
}
func (svc CalculatorServiceHandler) Sub(ctx context.Context, req *SubRequest) (*SubResponse, error) {
result := req.A - req.B
return &SubResponse{Result: result}, nil
}
Step 2: Generate Your RPC Client and Gateway
At this point, you've just written the same code that you (hopefully) would have written even if you weren't using Frodo. Next, we want to auto-generate two things:
- A "gateway" that allows an instance of your CalculatorService to listen for incoming requests (via an HTTP API).
- A "client" struct that communicates with that API to get work done.
Just run these two commands in a terminal:
# Feed it the service interface code, not the handler.
frodo gateway calculator_service.go
frodo client calculator_service.go
Step 3: Run Your Calculator API Server
Let's fire up an HTTP server on port 9000 that makes your service available for consumption (you can choose any port you want, obviously).
package main
import (
"net/http"
"github.com/your/project/calc"
calcrpc "github.com/your/project/calc/gen"
)
func main() {
service := calc.CalculatorServiceHandler{}
gateway := calcrpc.NewCalculatorServiceGateway(service)
http.ListenAndServe(":9000", gateway)
}
Seriously. That's the whole program.
Compile and run it, and your service/API is now ready to be consumed. We'll use the Go client we generated in just a moment, but you can try this out right now by simply using curl:
curl -d '{"A":5, "B":2}' http://localhost:9000/CalculatorService.Add
# {"Result":7}
curl -d '{"A":5, "B":2}' http://localhost:9000/CalculatorService.Sub
# {"Result":3}
Step 4: Consume Your Calculator Service
While you can use raw HTTP to communicate with the service, let's use our auto-generated client to hide the gory details of JSON marshaling, status code translation, and other noise.
The client actually implements CalculatorService just like the server/handler does. As a result the RPC-style call will "feel" like you're executing the service work locally, when in reality the client is actually making API calls to the server running on port 9000.
package main
import (
"context"
"fmt"
"log"
"github.com/your/project/calc"
"github.com/your/project/calc/gen"
)
func main() {
ctx := context.Background()
client := calcrpc.NewCalculatorServiceClient("http://localhost:9000")
add, err := client.Add(ctx, &calc.AddRequest{A:5, B:2})
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println("5 + 2 = ", add.Result)
sub, err := client.Sub(ctx, &calc.SubRequest{A:5, B:2})
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println("5 - 2 = ", sub.Result)
}
Compile/run this program, and you should see the following output:
5 + 2 = 7
5 - 2 = 3
That's it!
For more examples of how to write services that let Frodo take care of the RPC/API boilerplate, take a look in the example/ directory of this repo.
Doc Options: Custom URLs, Status, etc
Frodo gives you a remote service/API that "just works" out of the box. You can, however customize the API routes for individual operations, set a prefix for all routes in a service, and more using "Doc Options"... worst Spider-Man villain ever.
Here's an example with all the available options. They are all independent, so you can specify a custom status without specifying a custom route and so on.
// CalculatorService provides some basic arithmetic operations.
//
// VERSION 0.1.3
// PATH /v1
type CalculatorService interface {
// Add calculates the sum of A + B.
//
// HTTP 202
// GET /addition/:A/:B
Add(context.Context, *AddRequest) (*AddResponse, error)
// Sub calculates the difference of A - B.
//
// GET /subtraction/:A/:B
Sub(context.Context, *SubRequest) (*SubResponse, error)
}
Service: PATH
This prepends your custom value on every route in the API. It applies
to the standard ServiceName.FunctionName routes as well as custom routes
as we'll cover in a moment.
Your generated API and RPC clients will be auto-wired to use the prefix "v1" under the hood, so you don't need to change your code any further. If you want to hit the raw HTTP endpoints, however, here's how they look now:
curl -d '{"A":5, "B":2}' http://localhost:9000/v1/CalculatorService.Add
# {"Result":7}
curl -d '{"A":5, "B":2}' http://localhost:9000/v1/CalculatorService.Sub
# {"Result":3}
Function: GET/POST/PUT/PATCH/DELETE
You can replace the default POST ServiceName.FunctionName route for any
service operation with the route of your choice. In the example, the path parameters :A and :B
will be bound to the equivalent A and B attributes on the request struct.
Here are the updated curl calls after we generate the new gateway code. Notice it's also taking into account the service's PATH prefix as well:
curl http://localh
