Getting Started #
Twirp is an RPC framework from Twitch which just like gRPC uses Protobufs and is much easier to use. In this article I am going to give you a taste of Twirp. You can find the complete code for this article in this GitHub repository.
🙋♂️ This article presumes that you are familiar with Protobufs and the concept of RPC.
Setup Project #
Project creation and installation instructions for protoc
tool and twirp/proto
.
- Create a new Go project.
$ mkdir go-twirp-rpc-example
$ cd go-twirp-rpc-example
$ go mod init github.com/gurleensethi/go-twirp-rpc-example
Create three folders server
, client
and rpc
in the project so that the folder structure looks like this.
go-twirp-rpc-example/
├─ client/
├─ server/
├─ rpc/
├─ notes/
- Install the protoc and twirp-protoc generators (these are cli tools).
$ go install github.com/twitchtv/twirp/protoc-gen-twirp@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- Install protobuf and twirp modules in project.
$ go get github.com/twitchtv/twirp
$ go get google.golang.org/protobuf
Writing Proto file #
Since Twirp uses Protobufs to generate clients, we need to write a .proto
file.
You will be building a simple note taking application that only has two features:
1. Get all notes.
2. Create a new note.
Here is the proto definition model (store it in rpc/notes/service.proto
).
syntax = "proto3"; package gotwirprpcexample.rpc.notes; option go_package = "rpc/notes"; message Note { int32 id = 1; string text = 2; int64 created_at = 3; } message CreateNoteParams { string text = 1; } message GetAllNotesParams { } message GetAllNotesResult { repeated Note notes = 1; } service NotesService { rpc CreateNote(CreateNoteParams) returns (Note); rpc GetAllNotes(GetAllNotesParams) returns (GetAllNotesResult); }
rpc/notes/service.proto
If you don't understand the above code you need to learn about Protobufs here. In hindsight we have defined a Note type and defined two RPC calls CreateNote
(to create a new note) GetAllNotes
(to get all notes).
Generating Go code #
Now that proto file is in place, let's generate the go client using the protoc
generator.
From the root of your project run the protoc
command.
$ protoc --twirp_out=. --go_out=. rpc/notes/service.proto
This will generate two files service.pb.go
and service.twirp.go
in the rpc/notes
directory. While not required, it is a good idea to go through the generated code to get a better understanding of what is generated.
Writing Server Code #
Server side of things involves implementing the two functions CreateNote
and GetAllNotes
.
If you open the generated twirp code rpc/notes/service.twirp.go
, you will find an interface NotesService
, this is the interface that we will be implementing.
// ====================== // NotesService Interface // ====================== type NotesService interface { CreateNote(context.Context, *CreateNoteParams) (*Note, error) GetAllNotes(context.Context, *GetAllNotesParams) (*GetAllNotesResult, error) }
rpc/notes/service.twirp.go
Implementing functionality #
- Define a
notesService
type that holds a list of notes.
package main import ( "github.com/gurleensethi/go-twirp-rpc-example/rpc/notes" ) type notesService struct { Notes []notes.Note CurrentId int32 } func main() { }
server/main.go
- Implement the
CreateNote
function onnotesService
.
package main import ( "context" "net/http" "time" "github.com/gurleensethi/go-twirp-rpc-example/rpc/notes" "github.com/twitchtv/twirp" ) ... func (s *notesService) CreateNote(ctx context.Context, params *notes.CreateNoteParams) (*notes.Note, error) { if len(params.Text) < 4 { return nil, twirp.InvalidArgument.Error("Text should be min 4 characters.") } note := notes.Note{ Id: s.CurrentId, Text: params.Text, CreatedAt: time.Now().UnixMilli(), } s.Notes = append(s.Notes, note) s.CurrentId++ return ¬e, nil } ...
server/main.go
Pretty straightforward stuff. One thing to notice is we are returning twirp.InvalidArgument
in case the length of text is less than 4. Twirp errors let you easily communicate different states of errors to the client, if you return a regular error Twirp will wrap it in a twirp.InternalError
.
I encourage you to read the documentation on Twirp Errors here.
- Implement the
GetAllNotes
function onnotesService
.
func (s *notesService) GetAllNotes(ctx context.Context, params *notes.GetAllNotesParams) (*notes.GetAllNotesResult, error) { allNotes := make([]*notes.Note, 0) for _, note := range s.Notes { n := note allNotes = append(allNotes, &n) } return ¬es.GetAllNotesResult{ Notes: allNotes, }, nil }
server/main.go
Serving over HTTP #
Running a Twirp server is as easy as serving it using the deafult net/http
package 😀.
func main() { notesServer := notes.NewNotesServiceServer(¬esService{}) mux := http.NewServeMux() mux.Handle(notesServer.PathPrefix(), notesServer) err := http.ListenAndServe(":8000", notesServer) if err != nil { panic(err) } }
server/main.go
Run the server using go run server/main.go
, its as easy as that. Glance at the complete server code here.
Writing Client Code #
On the client side, let's create a new note and subsequently fetch all the notes.
- Prepare the notes service client.
package main import ( "net/http" "github.com/gurleensethi/go-twirp-rpc-example/rpc/notes" ) func main() { client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{}) }
client/main.go
- Call the
CreateNote
function.
package main import ( "context" "log" "net/http" "github.com/gurleensethi/go-twirp-rpc-example/rpc/notes" ) func main() { client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{}) ctx := context.Background() _, err := client.CreateNote(ctx, ¬es.CreateNoteParams{Text: "Hello World"}) if err != nil { log.Fatal(err) } }
client/main.go
- List all the notes using
GetAllNotes
.
func main() { client := notes.NewNotesServiceProtobufClient("http://localhost:8000", &http.Client{}) ctx := context.Background() _, err := client.CreateNote(ctx, ¬es.CreateNoteParams{Text: "Hello World"}) if err != nil { log.Fatal(err) } allNotes, err := client.GetAllNotes(ctx, ¬es.GetAllNotesParams{}) if err != nil { log.Fatal(err) } for _, note := range allNotes.Notes { log.Println(note) } }
client/main.go
Call RPC via HTTP #
A very lovely thing about Twirp is you can call the functions through simple HTTP calls. Everything underneath Twirp is a POST
request.
Request in Insomnia
cURL Request
❯ curl --request POST \
--url http://localhost:8000/twirp/gotwirprpcexample.rpc.notes.NotesService/GetAllNotes \
--header 'Content-Type: application/json' \
--data '{}'
{"notes":[{"id":0,"text":"Hello World","created_at":"1668035588211"}]}
Read more about using cURL with Twirp here.
Ending #
That was a quick introduction to Twirp, as next steps take a look at Errors and HTTP Headers.