Build a Book Catalog Microservice
Define a gRPC service contract, generate code, and stream real-time book updates.
Introduction
30 Second Summary
When apps need to talk to each other over a network, they need a shared language. REST APIs use JSON over HTTP, but there's a faster, stricter alternative designed for services that communicate constantly: gRPC.
In this project, you will build a book catalog microservice using gRPC and Protocol Buffers. You'll define a typed service contract, generate Python code from it, implement a server with real-time streaming, and test everything from the command line.
What You'll Build
A running book catalog where you add a book in one terminal and watch it appear instantly in another terminal's live feed.
By the end of this project, you'll have:
- A working gRPC server that accepts requests to add and retrieve books, enforced by a typed contract that catches mistakes before anything hits the network.
- A live streaming subscription that pushes new books to connected clients in real-time, without polling.
- Hands-on experience with the full gRPC development cycle: define a .proto contract, generate code, implement server logic, build a client, and test with grpcurl.
- Secret Mission: Add a ListBooks RPC with author filtering, proving you can extend a gRPC service end-to-end on your own.
Are there any prerequisites?
You should be comfortable with basic Python (functions, classes, loops) and have a general understanding of how clients and servers communicate over a network.
Before We Start
Before you write any code, take a moment to commit to what you're building and why. Understanding the problem gRPC solves makes every step ahead click into place.
Set Up Your Project
Your gRPC service needs two categories of tools: Python libraries for building the server and client, and a command-line testing tool for poking at the service once it's running.
In this step, you'll create a project directory, install the gRPC Python packages, and set up grpcurl (a tool that talks to gRPC servers the same way curl talks to REST APIs).
In this step, get ready to:
- Create a project directory with a Python virtual environment.
- Install gRPC Python libraries (grpcio and grpcio-tools).
- Install grpcurl for testing your gRPC server from the command line.
Create your project and install Python packages
The grpcio package is the core gRPC runtime for Python. The grpcio-tools package bundles the Protocol Buffers compiler so you can generate Python code from .proto files without installing anything extra.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir grpc-book-catalog
cd grpc-book-catalog
- Confirm you're in the right place by running:
pwd
You should see a path ending in grpc-book-catalog.
- Create a Python virtual environment, activate it, and install the gRPC packages by running these commands:
python3 -m venv venv
source venv/bin/activate
pip install grpcio==1.82.1 grpcio-tools==1.82.1
You'll see pip downloading and installing packages. The output ends with a line confirming the packages were successfully installed.
What do these commands do?
- python3 -m venv venv creates an isolated Python environment inside a venv folder. This keeps your project's packages separate from your system Python.
- source venv/bin/activate switches your terminal session to use the virtual environment. Your prompt changes to show (venv) at the start.
- pip install downloads the two gRPC libraries pinned to version 1.82.1.
Seeing a permission error or pip not found?
- Make sure you activated the virtual environment first. Your prompt should show (venv) at the beginning.
- If source doesn't work on Windows, use venv\Scripts\activate instead.
- help me fix a pip install error in my gRPC project virtual environment
Install grpcurl
grpcurl is a command-line tool that lets you interact with gRPC services without writing any client code. Think of it as the gRPC equivalent of curl for REST APIs.
- Check if grpcurl is already on your machine by running:
grpcurl --version
✔️ I see a version number
grpcurl is ready to go. Skip ahead to the next substep.
ⓧ Command not found
- Install grpcurl using Homebrew by running:
brew install grpcurl
Homebrew downloads and links the binary. This takes about a minute.
Seeing 'brew: command not found'?
You need Homebrew installed first. Visit brew.sh and follow the one-line install command, then re-run the grpcurl install.
help me install Homebrew and grpcurl on my machine
Verify both tools are working
Before you move on, confirm that both tools respond without errors.
What do you expect each command to print? Take a guess before running them.
- Verify the gRPC tools package by running:
python3 -m grpc_tools.protoc --version
You should see libprotoc 29.3 (or a similar version line). This confirms the Protocol Buffers compiler is bundled and ready.
- Verify grpcurl by running:
grpcurl --version
You should see grpcurl v1.9.3 (or the version Homebrew installed). Both tools are ready.
One of the commands failed?
- If the protoc command fails with a ModuleNotFoundError, make sure your virtual environment is still active. Check for (venv) in your prompt. If it's gone, run source venv/bin/activate again.
- If grpcurl isn't found, Homebrew may not have linked it. Run brew link grpcurl and try again.
- help me troubleshoot my gRPC tool installation
Your environment is locked and loaded. Next up, you'll write the service contract that defines exactly what your book catalog can do.
Define the Service Contract
Your tools are installed and ready to go. Now comes the most important decision in any gRPC project: defining the service contract.
Before writing any server or client code, you define what your service can do in a language-neutral .proto file. This single file becomes the source of truth that generates code for any programming language. Write it once, and the Protocol Buffers compiler produces type-safe client and server code in Python, Go, Java, Rust, or any other supported language.
In this step, get ready to:
- Write a .proto file defining your BookCatalog service with three RPC methods.
- Generate Python code from the contract using the protoc compiler.
- Examine the generated code to understand the artifacts you will use in the next step.
Write the .proto file
A .proto file declares the service's methods and the shape of every message that flows between client and server. Think of it as the API contract that both sides agree on before any code is written.
- Create a new file called book_catalog.proto in your grpc-book-catalog folder by running:
touch book_catalog.proto
- Confirm the file was created by running:
ls
You should see book_catalog.proto alongside your venv folder.
- Open book_catalog.proto in VSCodium and paste the following service definition:
syntax = "proto3";
package book_catalog;
// Define the BookCatalog service with its available RPCs
service BookCatalog {
// Unary RPC: client sends one request, server sends one response
rpc AddBook (AddBookRequest) returns (Book);
rpc GetBook (GetBookRequest) returns (Book);
// Server streaming: client sends one request, server sends a stream of responses
rpc WatchNewBooks (WatchNewBooksRequest) returns (stream Book);
}
// Request to add a new book to the catalog
message AddBookRequest {
string title = 1;
string author = 2;
}
// Request to retrieve a book by its unique ID
message GetBookRequest {
string id = 1;
}
// Empty request - watching doesn't need any parameters
message WatchNewBooksRequest {}
// The Book message represents a single book in the catalog
message Book {
string id = 1;
string title = 2;
string author = 3;
}
What does this contract define?
This file defines three things:
- A service block listing the RPC methods your server exposes. AddBook and GetBook are unary (one request, one response). WatchNewBooks uses the stream keyword, meaning the server can push multiple responses back over time.
- Message types that define the exact fields each request and response carries. The numbers (= 1, = 2) are field tags used for binary encoding, not default values.
- A package name that namespaces everything to avoid collisions with other services.
- Save book_catalog.proto.
Syntax highlighting not working?
VSCodium may not recognise .proto files by default. The content will still work perfectly without highlighting. If you want syntax colours, search for a Protocol Buffers extension in the extensions panel.
Help me check my .proto file
✔️ Awesome, I've got everything!
Great. Make sure you have saved book_catalog.proto before moving on.
ⓧ I'd like to double check the full code
Your book_catalog.proto should contain exactly this:
syntax = "proto3";
package book_catalog;
service BookCatalog {
rpc AddBook (AddBookRequest) returns (Book);
rpc GetBook (GetBookRequest) returns (Book);
rpc WatchNewBooks (WatchNewBooksRequest) returns (stream Book);
}
message AddBookRequest {
string title = 1;
string author = 2;
}
message GetBookRequest {
string id = 1;
}
message WatchNewBooksRequest {}
message Book {
string id = 1;
string title = 2;
string author = 3;
}
Generate Python code from the contract
The protoc compiler reads your .proto file and generates language-specific code. Since grpcio-tools bundles protoc, you can run it directly through Python with no extra installation.
- Generate the Python stubs by running this command:
python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --grpc_python_out=. book_catalog.proto
What do these flags mean?
- -I. tells protoc to look in the current directory for .proto imports.
- --python_out=. generates the message classes (_pb2.py file).
- --pyi_out=. generates type hint stubs (_pb2.pyi file) for better IDE support.
- --grpc_python_out=. generates the gRPC service and client classes (_pb2_grpc.py file).
Before you check the result, predict: how many new files do you expect to see in the directory?
- Confirm the generated files exist by running:
ls
You should see three new files alongside your proto file: book_catalog_pb2.py, book_catalog_pb2.pyi, and book_catalog_pb2_grpc.py.
No files appeared?
If you see an error or no new files were created:
- Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
- Check that book_catalog.proto is saved in the same directory where you are running the command. Run ls and confirm you see it listed.
- Look for typos in the proto file. A missing semicolon or misspelled keyword will cause protoc to fail with an error pointing to the line number.
Help me fix a protoc generation error
Examine the generated code
You never edit generated files directly. But understanding what is inside them helps you use them correctly in the next step.
- Verify the generated code is importable by running this command:
python3 -c "from book_catalog_pb2_grpc import BookCatalogStub, BookCatalogServicer, add_BookCatalogServicer_to_server; print('All three artifacts imported successfully')"
You should see All three artifacts imported successfully printed to your terminal.
What are these three artifacts?
The generated book_catalog_pb2_grpc.py file contains three key pieces you will use throughout this project:
- BookCatalogStub is the client class. Your Python client will use this to call the server's methods over the network.
- BookCatalogServicer is the server interface. You will subclass this and fill in the actual logic for each RPC method.
- add_BookCatalogServicer_to_server is the wiring function. It connects your implementation to a running gRPC server so it can receive requests.
The book_catalog_pb2.py file contains the message classes (AddBookRequest, GetBookRequest, WatchNewBooksRequest, and Book) that represent the data flowing between client and server.
Seeing an ImportError?
If the import fails, the generated files may be missing or incomplete:
- Run ls and confirm book_catalog_pb2_grpc.py exists in the current directory.
- If it is missing, re-run the protoc generation command from the previous substep.
- Make sure you are running the import from the same directory that contains the generated files.
Help me fix my import error with the generated gRPC code
Your service contract is defined and the generated code is ready. Next up, you will implement the actual server logic that brings these method definitions to life.
Implement and Run the Server
You have a .proto contract and generated Python code, but no actual business logic yet.
The generated BookCatalogServicer class is an empty interface. In this step, you fill in the real behavior for each RPC method and start the server so clients can connect.
In this step, get ready to:
- Implement AddBook and GetBook as unary RPC methods.
- Implement WatchNewBooks as a server-streaming RPC.
- Start the gRPC server with reflection and verify it is discoverable.
Implement the BookCatalogServicer
Every gRPC server subclasses the generated servicer interface and fills in each method with real logic. Your AddBook method stores a book in memory and notifies any active stream watchers.
- Create a new file called server.py in your grpc-book-catalog folder.
- Add the imports, class definition, and AddBook method by pasting this code:
import threading
import uuid
from concurrent import futures
import grpc
from grpc_reflection.v1alpha import reflection
import book_catalog_pb2
import book_catalog_pb2_grpc
class BookCatalogServicer(book_catalog_pb2_grpc.BookCatalogServicer):
def __init__(self):
# In-memory store for books
self.books = {}
# List of active stream watchers
self.watchers = []
# Thread lock for safe concurrent access
self.lock = threading.Lock()
def AddBook(self, request, context):
# Generate a short unique ID for the new book
book_id = str(uuid.uuid4())[:8]
book = book_catalog_pb2.Book(
id=book_id,
title=request.title,
author=request.author,
)
with self.lock:
self.books[book_id] = book
# Notify all active stream watchers about the new book
for watcher in self.watchers:
watcher["queue"].append(book)
watcher["event"].set()
return book
What does this code do?
- The class subclasses the generated BookCatalogServicer interface, which means the gRPC framework knows how to route incoming requests to your methods.
- self.books is a dictionary acting as an in-memory database. Each book gets an 8-character UUID as its key.
- self.watchers holds references to any clients currently subscribed to the live stream. When a new book is added, every watcher gets notified.
- The threading.Lock ensures that multiple clients adding books at the same time don't corrupt the shared data.
Now add the GetBook method, which looks up a book by ID and returns a proper gRPC status code if it is missing.
- Add this method directly below AddBook inside the BookCatalogServicer class:
def GetBook(self, request, context):
with self.lock:
book = self.books.get(request.id)
if book is None:
# Return NOT_FOUND status code when the book doesn't exist
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"Book with id '{request.id}' not found")
return book_catalog_pb2.Book()
return book
What does GetBook do?
It looks up the book ID in the dictionary. If the book exists, it returns it directly. If not, it sets the response status code to NOT_FOUND with a human-readable message.
This is how gRPC handles errors. Instead of HTTP status codes like 404, gRPC has its own set of typed status codes (NOT_FOUND, INVALID_ARGUMENT, INTERNAL) that carry the same meaning across any programming language.
ask me
- Save server.py.
- Verify the class loads without errors by running:
python3 -c "from server import BookCatalogServicer; print('BookCatalogServicer loaded successfully')"
You should see BookCatalogServicer loaded successfully printed in your terminal.
Seeing an ImportError?
- Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
- Check that server.py is in the same folder as book_catalog_pb2.py and book_catalog_pb2_grpc.py. All files must be in the grpc-book-catalog directory.
- help me debug this ImportError
Implement WatchNewBooks with server streaming
Server streaming means the server keeps a connection open and pushes data to the client as it becomes available. Your WatchNewBooks method uses Python's yield keyword to send each new book down the stream whenever one is added.
- Add the WatchNewBooks method below GetBook inside the BookCatalogServicer class:
def WatchNewBooks(self, request, context):
# Register this client as an active watcher
watcher = {"queue": [], "event": threading.Event()}
with self.lock:
self.watchers.append(watcher)
try:
# Keep the stream open as long as the client is connected
while context.is_active():
watcher["event"].wait(timeout=1.0)
watcher["event"].clear()
# Yield any new books that arrived since last check
while watcher["queue"]:
book = watcher["queue"].pop(0)
yield book
finally:
# Clean up when the client disconnects
with self.lock:
self.watchers.remove(watcher)
How does the streaming pattern work?
Each client that calls WatchNewBooks gets its own watcher dictionary with a queue and an event. The method loops forever, sleeping until the event fires.
When AddBook stores a new book, it appends the book to every watcher's queue and signals their event. The sleeping WatchNewBooks wakes up, pops the book from its queue, and yields it to the client.
The finally block ensures the watcher is removed from the list when the client disconnects, preventing memory leaks.
Start the gRPC server with reflection
The servicer class holds your business logic, but it needs a gRPC server to listen for connections. You also enable reflection so tools like grpcurl can discover your service without needing the .proto file.
- Add the serve() function and main block at the bottom of server.py, outside the class:
def serve():
# Create a gRPC server with a thread pool for handling requests
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
book_catalog_pb2_grpc.add_BookCatalogServicer_to_server(
BookCatalogServicer(), server
)
# Register service names for reflection (allows discovery)
service_names = (
book_catalog_pb2.DESCRIPTOR.services_by_name["BookCatalog"].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(service_names, server)
# Listen on port 50051 (gRPC default)
server.add_insecure_port("[::]:50051")
server.start()
print("Book catalog server started on port 50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()
What does the serve function do?
- ThreadPoolExecutor(max_workers=10) creates a pool of threads so the server can handle multiple clients at once.
- add_BookCatalogServicer_to_server wires your implementation class to the server's request router.
- enable_server_reflection registers your service so grpcurl can list available methods without the .proto file.
- [::]:50051 listens on all network interfaces at port 50051 (the gRPC default port).
- Save server.py.
- Start the server by running:
python3 server.py
You should see Book catalog server started on port 50051 printed in your terminal. The server is now running and waiting for connections. It will keep running until you stop it with Ctrl+C.
Why does the terminal hang?
The server is running in the foreground. It is actively listening for gRPC requests. This is expected. You need a second terminal to send requests to it.
Before you test the next command, what do you think grpcurl -plaintext localhost:50051 list will show you?
- Open a new terminal window.
- Activate the virtual environment in the new terminal by running:
cd ~/Desktop/grpc-book-catalog
source venv/bin/activate
- Verify your server is discoverable by running:
grpcurl -plaintext localhost:50051 list
You should see book_catalog.BookCatalog printed as one of the service names. This confirms your server is live and its API is discoverable via reflection.
Seeing 'Failed to dial target host'?
- Check that your server is still running in the other terminal. You should see Book catalog server started on port 50051 there with no error messages after it.
- Make sure you did not accidentally close the first terminal or press Ctrl+C in it.
- If you see port already in use, another process is using port 50051. Stop it with lsof -i :50051 to find the PID, then kill <PID> to free the port.
- help me connect grpcurl to my server
✔️ Awesome, I've got everything!
Great. Make sure you have saved server.py and that your server is still running in its terminal.
ⓧ I'd like to double check the full code
Your server.py should contain four sections, in this order:
- Imports (threading, uuid, futures, grpc, reflection, and the two generated modules).
- The BookCatalogServicer class with __init__, AddBook, GetBook, and WatchNewBooks methods.
- The serve() function that creates the server, enables reflection, and starts listening on port 50051.
- The if __name__ == "__main__" block that calls serve().
Scroll up and confirm each code chunk is present in your file, in the order listed above.
Your server is running and discoverable. Next up, you will fire actual RPC requests at it with grpcurl and see how AddBook and GetBook behave in practice.
Test with grpcurl
Your server is running and discoverable. Now it's time to actually talk to it.
In this step, you'll fire requests at your gRPC service using grpcurl. You'll test both the unary RPCs (single request, single response) and the streaming RPC (connection stays open, pushing data in real-time).
In this step, get ready to:
- Add a book and retrieve it by ID using unary RPCs.
- Test error handling by requesting a nonexistent book.
- Subscribe to WatchNewBooks and see books appear in real-time as they are added.
Test AddBook and GetBook
With your server running in one terminal, you'll open a second terminal to send requests. grpcurl takes JSON input and sends it as a typed Protocol Buffer message to the method you specify.
- Open a new terminal tab or window. Your server must keep running in the first terminal.
- Add a book to your catalog by running this command:
grpcurl -plaintext -d '{"title": "Dune", "author": "Frank Herbert"}' localhost:50051 book_catalog.BookCatalog/AddBook
What does this command do?
- -plaintext tells grpcurl to skip TLS (fine for local development).
- -d '{...}' provides the request data as JSON. grpcurl converts it to a Protocol Buffer message automatically.
- The path book_catalog.BookCatalog/AddBook is the fully qualified service name plus the method. grpcurl discovers this via reflection.
You should see a JSON response containing an id field (a short generated string), plus the title and author you sent.
- Copy the id value from the response. You'll use it to retrieve the book.
- Retrieve the book by its ID by running this command:
grpcurl -plaintext -d '{"id": "[[BOOK_ID="the id from your AddBook response"]]"}' localhost:50051 book_catalog.BookCatalog/GetBook
You should see the same book returned with its full details.
- Now test what happens when a book doesn't exist. Request a nonexistent ID by running:
grpcurl -plaintext -d '{"id": "nonexistent"}' localhost:50051 book_catalog.BookCatalog/GetBook
grpcurl displays the NOT_FOUND status code and the details message your server set. This is gRPC's equivalent of an HTTP 404 response.
Why status codes instead of HTTP codes?
gRPC defines its own set of status codes (like NOT_FOUND, INVALID_ARGUMENT, INTERNAL) that work across all transport layers. They're richer and more specific than HTTP status codes.
Seeing 'Failed to dial target host'?
- Make sure your server is still running in the other terminal. You should see Book catalog server started on port 50051 printed there.
- Check that you included -plaintext in your grpcurl command. Without it, grpcurl tries to use TLS and fails against your unencrypted server.
Why can't grpcurl reach my server?
Test WatchNewBooks streaming
The unary RPCs follow a simple request-response pattern. Server streaming is different. The client sends one request, and the server keeps the connection open, pushing new data whenever it becomes available.
Before you run the next command, think about what you expect to happen. You're calling a method that streams new books. What should grpcurl do if no new books are being added right now?
- Start the WatchNewBooks stream by running this command:
grpcurl -plaintext -d '{}' localhost:50051 book_catalog.BookCatalog/WatchNewBooks
The command hangs. It doesn't print anything and doesn't return to your prompt. That's exactly right. The stream is open, waiting for new books to arrive.
Why does it hang?
Unlike a unary RPC that returns one response and closes, a server-streaming RPC keeps the connection alive. The server will push data through this open connection whenever something happens. grpcurl stays connected, waiting to print whatever the server sends.
- Leave that terminal running. Open a third terminal tab or window.
- From this third terminal, add a new book by running:
grpcurl -plaintext -d '{"title": "Neuromancer", "author": "William Gibson"}' localhost:50051 book_catalog.BookCatalog/AddBook
- Now switch back to the terminal running WatchNewBooks.
The book you just added appears in the watch terminal. The server pushed it through the open stream the instant it was stored. No polling. No second request. The data arrived in real-time.
- Try adding one more book from your third terminal to confirm the stream keeps delivering:
grpcurl -plaintext -d '{"title": "Snow Crash", "author": "Neal Stephenson"}' localhost:50051 book_catalog.BookCatalog/AddBook
- Check your watch terminal again. The second book also appeared immediately.
- Press Ctrl+C in the watch terminal to stop the stream.
What just happened?
You just witnessed the core value of server streaming. One client opened a long-lived connection. Without sending any additional requests, it received every new book the moment another client added one.
With REST, you'd need to poll repeatedly ("any new books? any new books?"). With gRPC streaming, the server tells you when something happens.
Stream returned immediately with no output?
- If WatchNewBooks exits immediately instead of hanging, your server may have crashed and restarted. Check the server terminal for errors and restart it with python3 server.py if needed.
- Make sure you're adding books AFTER starting the watch. Books added before the stream opened won't appear.
Why doesn't my WatchNewBooks stream show any books?
You've proven that your service works end-to-end using a generic command-line tool. Next up, you'll build a Python client that connects to the same server and subscribes to the live book feed programmatically.
Build the Python Client
Your server now handles both unary calls and live streaming correctly, all tested through grpcurl. But a generic command-line tool is no substitute for a real client that other services could use.
In this step, you build a Python client that talks to the server using the generated BookCatalogStub. The client will add books, look them up by ID, and subscribe to the real-time stream. A streaming RPC client turns out to be surprisingly simple: it is just a for-loop over the stub call.
In this step, get ready to:
- Build a Python client with unary RPC calls for adding and retrieving books.
- Add a streaming watch command that receives new books in real-time.
- Demo the full system with multiple terminals to see server-streaming in action.
Build a Python client with unary calls
The generated BookCatalogStub class gives you a ready-made client interface. Each RPC method on the server has a corresponding method on the stub that handles serialization, the network call, and deserialization for you.
- Create a new file called client.py in your grpc-book-catalog folder and add the imports and unary call functions:
import sys
import grpc
import book_catalog_pb2
import book_catalog_pb2_grpc
def add_book(stub, title, author):
# Send an AddBook request and print the created book
response = stub.AddBook(
book_catalog_pb2.AddBookRequest(title=title, author=author)
)
print(f"Added book: id={response.id}, title={response.title}, author={response.author}")
def get_book(stub, book_id):
# Attempt to retrieve a book by ID, handling not-found errors
try:
response = stub.GetBook(book_catalog_pb2.GetBookRequest(id=book_id))
print(f"Found book: id={response.id}, title={response.title}, author={response.author}")
except grpc.RpcError as e:
print(f"Error: {e.code().name} - {e.details()}")
What does this code do?
- add_book creates an AddBookRequest message, sends it to the server, and prints the response. The generated stub handles all the serialization to binary.
- get_book wraps the call in a try/except for grpc.RpcError. This catches gRPC status codes like NOT_FOUND and prints them cleanly instead of crashing.
Now these functions need a way to be called from the command line. The main function parses command-line arguments and routes them to the right function.
- Add the main function below get_book in client.py:
def main():
if len(sys.argv) < 2:
print("Usage: python3 client.py <command> [args]")
print("Commands: add <title> <author> | get <id> | watch")
sys.exit(1)
# Connect to the gRPC server and create a stub
channel = grpc.insecure_channel("localhost:50051")
stub = book_catalog_pb2_grpc.BookCatalogStub(channel)
command = sys.argv[1]
if command == "add":
if len(sys.argv) < 4:
print("Usage: python3 client.py add <title> <author>")
sys.exit(1)
add_book(stub, sys.argv[2], sys.argv[3])
elif command == "get":
if len(sys.argv) < 3:
print("Usage: python3 client.py get <id>")
sys.exit(1)
get_book(stub, sys.argv[2])
else:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()
What does this code do?
- grpc.insecure_channel("localhost:50051") opens a connection to the server. The stub wraps this channel and exposes typed methods matching your .proto service definition.
- The argument parsing routes add and get to the corresponding functions. Each validates it has enough arguments before calling the stub.
- Save client.py.
- Make sure your server is still running in another terminal. Test the client by adding a book:
python3 client.py add "Dune" "Frank Herbert"
You should see output like:
Added book: id=a1b2c3d4, title=Dune, author=Frank Herbert
The ID will be different since it is randomly generated. Copy that ID and test the get command:
python3 client.py get [[BOOK_ID="the ID from the add response above"]]
You should see the full book details printed back.
Seeing 'Connection refused'?
- Make sure your server is still running in the other terminal. You should see the 'Book catalog server started on port 50051' message there.
- Check that your virtual environment is activated in the terminal where you run the client (you should see (venv) in the prompt).
Why is my client getting a connection error?
Add the watch command with streaming
On the server side, streaming required threading and event queues. On the client side, it is dramatically simpler. A server-streaming RPC returns an iterator. You consume it with a regular for loop.
- In client.py, add the watch_books function between get_book and main:
def watch_books(stub):
print("Watching for new books (Ctrl+C to stop)...")
try:
# Iterate over the server stream - each iteration blocks
# until the server yields a new book
for book in stub.WatchNewBooks(book_catalog_pb2.WatchNewBooksRequest()):
print(f"New book added: id={book.id}, title={book.title}, author={book.author}")
except grpc.RpcError as e:
print(f"Stream ended: {e.code().name} - {e.details()}")
except KeyboardInterrupt:
print("\nStopped watching.")
What does this code do?
- stub.WatchNewBooks() returns a response iterator. The for book in ... loop blocks on each iteration, waiting for the server to yield the next book.
- The KeyboardInterrupt handler lets the learner press Ctrl+C to stop watching gracefully.
Now wire up the watch command in main.
- In the main function, find this block:
elif command == "get":
if len(sys.argv) < 3:
print("Usage: python3 client.py get <id>")
sys.exit(1)
get_book(stub, sys.argv[2])
else:
print(f"Unknown command: {command}")
sys.exit(1)
- Add an elif branch for watch between the get block and the else, so it looks like this:
elif command == "get":
if len(sys.argv) < 3:
print("Usage: python3 client.py get <id>")
sys.exit(1)
get_book(stub, sys.argv[2])
elif command == "watch":
watch_books(stub)
else:
print(f"Unknown command: {command}")
sys.exit(1)
- Save client.py.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Your client.py should contain these sections in order. Scroll up and confirm each one is present:
- Imports (sys, grpc, book_catalog_pb2, book_catalog_pb2_grpc).
- The add_book function.
- The get_book function with grpc.RpcError handling.
- The watch_books function with the for-loop over the stream.
- The main function routing add, get, and watch commands.
- The if __name__ == "__main__": block at the bottom.
Demo the full system
This is the payoff. You are about to see real-time server-streaming in action across multiple terminals.
- Open a new terminal tab or window (your server should still be running in the first terminal).
- Activate the virtual environment and start the watcher by running:
cd ~/Desktop/grpc-book-catalog
source venv/bin/activate
python3 client.py watch
You should see:
Watching for new books (Ctrl+C to stop)...
The terminal is now blocking, waiting for the server to push new books. Before you do anything else, predict: what do you think will happen in this terminal if you add a book from a different terminal?
- Open a third terminal tab or window.
- Activate the virtual environment and add a book using grpcurl:
cd ~/Desktop/grpc-book-catalog
source venv/bin/activate
grpcurl -plaintext -d '{"title": "Dune", "author": "Frank Herbert"}' localhost:50051 book_catalog.BookCatalog/AddBook
Now switch back to the terminal running python3 client.py watch.
You should see the new book appear immediately:
New book added: id=a1b2c3d4, title=Dune, author=Frank Herbert
The book appeared without the watching client asking for it. The server pushed it the moment another client added it. This is server-streaming in action.
- Try adding a second book from the third terminal to see it appear instantly in the watcher:
grpcurl -plaintext -d '{"title": "Neuromancer", "author": "William Gibson"}' localhost:50051 book_catalog.BookCatalog/AddBook
- Confirm the watcher terminal now shows both books.
- Press Ctrl+C in the watching terminal to stop the stream gracefully.
Why is this different from polling?
With REST, the client would need to repeatedly ask "any new books?" on a timer. With server-streaming gRPC, the client opens the stream once and the server pushes data the instant it is available. Zero wasted requests, zero delay.
Not seeing books appear in the watcher?
- Make sure you restarted the server after adding the streaming fix in the previous step. The server must have the event-based WatchNewBooks implementation.
- Check that the watching client connects to the same port (50051) as the server.
- If the watch command exits immediately instead of blocking, confirm that the watch_books function is defined and the elif branch in main calls it.
Why aren't new books showing up in my streaming client?
Secret mission
Add ListBooks with Author Filtering
Your catalog service can add, retrieve, and stream books. But what about searching? Real catalog services need filtering. In this mission, you'll add a ListBooks RPC that returns all books, optionally filtered by author, reinforcing the full gRPC development cycle end-to-end.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The grpc-book-catalog project directory on your Desktop (includes your virtual environment, source code, and generated files).
- A gRPC server process running on port 50051.
Keep everything running
No action needed. Choose this if you're still actively building or experimenting with your gRPC service.
- Your project directory stays on your Desktop for reference and experimentation.
- The server continues running on port 50051 until you stop it or restart your machine.
- You can keep testing with grpcurl or client.py at any time.
Pause - I'll come back to this later
Shut down the running server process to free up your terminal. You can restart everything later in seconds.
- Press Ctrl+C in the terminal running your server to stop it.
- When you're ready to come back, navigate to your project directory and reactivate by running:
cd ~/Desktop/grpc-book-catalog
source venv/bin/activate
python3 server.py
Your books are stored in memory, so the catalog starts empty each time you restart the server.
Delete - I don't want to use this again
Remove all project files and start fresh. This cannot be undone.
- Press Ctrl+C in the terminal running your server to stop it.
- Deactivate the virtual environment and delete the project directory by running:
deactivate
cd ~/Desktop
rm -rf grpc-book-catalog
On Windows, use this instead:
deactivate
cd ~\Desktop
rmdir /s /q grpc-book-catalog
- Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir ~\Desktop (Windows). You should no longer see grpc-book-catalog in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully functional gRPC microservice from scratch, starting from a language-neutral contract and ending with real-time streaming between multiple terminals.
You've learned how to:
- Build a complete gRPC service following the contract-first workflow: define a .proto file, generate type-safe Python code, and implement business logic that any language's client can call.
- Implement both unary RPCs (request-response) and server-streaming RPCs (real-time push) with proper error handling using gRPC status codes.
- Use grpcurl and gRPC reflection to discover and test services from the command line, without needing client code or the original .proto file.
- Secret Mission: Extended the service with a new ListBooks RPC end-to-end, reinforcing the full development cycle of modifying the contract, regenerating code, and implementing logic.
Ready to quiz yourself?