Evans is a wonderfully simple tool for interacting with gRPC services directly from your terminal, but its real magic lies in how it abstracts away the complexities of protobuf definitions and network communication, letting you treat a remote service like a local REPL.
Let’s see it in action. Imagine we have a simple gRPC service defined in user.proto for managing users:
syntax = "proto3";
package user;
message User {
string id = 1;
string name = 2;
string email = 3;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message GetUserRequest {
string id = 1;
}
service UserService {
rpc CreateUser (CreateUserRequest) returns (User);
rpc GetUser (GetUserRequest) returns (User);
}
To interact with this service using Evans, you first need to start the Evans REPL, pointing it to your gRPC server and providing the path to the compiled protobuf definitions. If your user.proto file is in the current directory and your UserService is running on localhost:50051, you’d start Evans like this:
evans --host localhost --port 50051 --proto ./user.proto
Once Evans starts, you’ll see a prompt like user.UserService>, indicating that you’re connected to the UserService and ready to call its methods. Now, you can call methods directly. To create a user, you’d type:
user.UserService> call CreateUser
{
"name": "Alice",
"email": "alice@example.com"
}
Evans parses your input, serializes it into the CreateUserRequest protobuf message, sends it to the server, receives the User response, and pretty-prints it for you. To retrieve that user, you’d use the GetUser method:
user.UserService> call GetUser
{
"id": "some-unique-id-from-create-response"
}
You can also inspect the service definition. Typing show services will list available services, and show calls within a service context will show available RPC methods.
The core problem Evans solves is the friction in testing and debugging gRPC APIs. Without it, you’d typically need to write client code in a specific language, compile it, and run it, which is cumbersome for quick checks. Evans bypasses this by allowing direct, interactive calls using the service’s own schema. It handles the serialization/deserialization of protobuf messages and the underlying HTTP/2 framing for gRPC, so you only need to worry about the request payload and interpreting the response.
Internally, Evans works by loading the .proto files you provide. It uses the protoc compiler (or a bundled version) to parse these definitions and build an in-memory representation of your gRPC services, messages, and fields. When you type a command like call CreateUser, Evans looks up the CreateUser method in its loaded schema, parses your JSON input into the corresponding CreateUserRequest protobuf structure, establishes a gRPC connection to the specified host and port, sends the request, and then deserializes the server’s response back into a human-readable format.
The most surprising thing about Evans is how effortlessly it handles complex, nested protobuf messages. You can pass deeply nested structures in JSON to Evans, and it will correctly map them to the corresponding protobuf message types without requiring any explicit mapping logic on your part, as long as your JSON structure mirrors the protobuf definition. For example, if CreateUserRequest had a nested Address message, you could provide it directly in the JSON input:
{
"name": "Bob",
"email": "bob@example.com",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}
Evans will automatically populate the nested address field of the CreateUserRequest object.
This ability to interact with any gRPC service without writing boilerplate client code makes Evans an indispensable tool for developers and SREs working with microservices.
The next step is exploring how to manage multiple protobuf files and more advanced features like streaming RPCs.