{Getting Buff}
Modernizing APIs with gRPC
Agenda
- Brief history of GraphQL
- GraphQL pros & cons
- Introduction to gRPC
- Brief history of gRPC
- GraphQL pros & cons
- A simple code example
- Wrap-up
Brief history of GraphQL
2007 - Facebook Query Language created
2012 - GraphQL created internally as FQL successor
2015 - Facebook announces Relay
2019 and beyond - GraphQL eats the world
- Strong typing solved (OpenAPI, JSON Schema)
- Declarative fetching was solved (field sets)
- Shaky community support
- Evolution = Versioning
- Dev learning curve
Cons
- Strongly-typed schema
- Declarative data fetching
- Faster than JSON REST
- Evolution, not Versioning
Pros
Benefits & Drawbacks of GraphQL
What's wrong with this code?
@UseGuards(AuthGuard)
@Resolver((of) => Book)
export class BooksResolver {
constructor(private bookssService: BooksService) {}
@Query((returns) => Book[])
async books() {
return this.bookssService.findAll()
}
@ResolveField()
async favorite(@Parent() book: Book, @CurrentAuthenticatedUser() currentUser: AuthenticatedUser) {
const { id } = book
return this.bookssService.isFavorite({ id, userId: currentUser.id })
}
}
# PRESENTING CODE
Do you need GraphQL
GraphQL is an alternative to REST for developing APIs, not a replacement.
- REST can do much of what GraphQL does
- GraphQL makes some tasks more complex
Intro to gRPC
A high performance, open source universal RPC framework
- Created and used by Google internally
- Message exchange is in a pre-defined binary format called protocol buffers.
- Useful for connecting polyglot microservices and mobile devices and browser clients
Brief history of gRPC
2011 - Stubby RPC open-sourced by Google
2015 - The next version of Stubby planned
2016 - Google announces gRPC
2017 and beyond - gRPC eats the world
gRPC: The main usage scenarios
- Efficiently connecting polyglot services in microservices style architecture
-
Connecting mobile devices, browser clients to backend services
- Generating efficient client libraries
- DevOps learning curve
- Webdev popularity
- Official Language support (11 languages)
Cons
- Strongly-typed schema
- High efficiency
- Bi-directional streaming
- Pluggable auth, tracing, etc.
- Just HTTP
Pros
Benefits & Drawbacks of gRPC
Introduction to Connect
- Interoperable protocol built by Buff
- Debuggable with general-purpose HTTP tools
- Backend framework integrations (Fastify, Next, etc.)
- Frontend clients (JS, Kotlin, Swift)
- Backwards compatible with gRPC's HTTP/2 protocol
- Will be accepted as a CNFC Sandbox project
Show me the code!
Creating the backend API
mkdir connect-api
cd connect-api
npm init -y
npm install typescript tsx
npx tsc --init
# PRESENTING CODE
Installing backend dependencies
npm install @bufbuild/buf @bufbuild/protoc-gen-es @bufbuild/protobuf
npm install @connectrpc/protoc-gen-connect-es @connectrpc/connect
npm install fastify @connectrpc/connect-fastify
# PRESENTING CODE
Creating your first protobuf
syntax = "proto3";
package connectrpc.eliza.v1;
service ElizaService {
rpc Books(Empty) returns (BooksResponse) {}
}
message BooksResponse {
repeated Book books = 1;
}
message Book {
uint32 id = 1;
string title = 2;
string description = 3;
string author = 4;
Timestamp publication_date = 5;
string genre = 6;
uint32 copies = 7;
}
message Timestamp {
int64 seconds = 1;
int32 nanos = 2;
}
message Empty {}
# PRESENTING CODE
Comparing protobuf to GraphQL
syntax = "proto3";
package connectrpc.eliza.v1;
service ElizaService {
rpc Books(Empty) returns (BooksResponse) {}
}
message BooksResponse {
repeated Book books = 1;
}
message Book {
uint32 id = 1;
string title = 2;
string description = 3;
string author = 4;
Timestamp publication_date = 5;
string genre = 6;
uint32 copies = 7;
}
message Timestamp {
int64 seconds = 1;
int32 nanos = 2;
}
message Empty {}
# PRESENTING CODE
type Query {
books: [Book!]
}
type Book {
id: Int
title: String
description: String
author: String
publication_date: DateTime
genre: String
copies: Int
}
scalar DateTime
Generating classes
npx buf generate proto
# PRESENTING CODE
Wiring up the backend service
import { ConnectRouter } from "@connectrpc/connect";
import { ElizaService } from "./gen/eliza_connect";
import { Book } from "./gen/eliza_pb";
export default (router: ConnectRouter) =>
router.service(ElizaService, {
async books(req) {
return { books };
},
});
const books: Book[] = [new Book({ title: "", description: "" })];
# PRESENTING CODE
Wiring up the backend service cont'd
import { FastifyInstance, fastify } from "fastify";
import { fastifyConnectPlugin } from "@connectrpc/connect-fastify";
import cors from "@fastify/cors";
import { cors as connectCors } from "@connectrpc/connect";
import { readFileSync } from "fs";
import routes from "./connect";
async function main() {
const config = {
logger: true,
http2: true,
};
const httpServer: any = fastify({
...config,
https: {
key: readFileSync("localhost+2-key.pem", "utf8"),
cert: readFileSync("localhost+2.pem", "utf8"),
},
});
const httpsServer: any = fastify(config);
setupServer(httpServer);
setupServer(httpsServer);
httpServer.listen({ port: 3001 });
httpsServer.listen({ port: 3002 });
console.log(
"server is listening at",
httpServer.addresses(),
httpsServer.addresses()
);
}
async function setupServer(server: FastifyInstance) {
server.get("/health", function (_, reply) {
reply.send();
});
await server.register(cors, {
methods: [...connectCors.allowedMethods],
allowedHeaders: [...connectCors.allowedHeaders],
exposedHeaders: [...connectCors.exposedHeaders],
});
await server.register(fastifyConnectPlugin, {
routes,
});
}
void main();
# PRESENTING CODE
Creating the frontend application
dart pub add riverpod flutter_riverpod grpc protobuf
# PRESENTING CODE
Creating the gRPC web client
import 'package:grpc/grpc_web.dart';
final api = GrpcWebClientChannel.xhr(Uri.parse('https://localhost:3001'));
# PRESENTING CODE
Creating the gRPC mobile/desktop client
import 'package:grpc/grpc.dart';
final api = ClientChannel(
'localhost',
port: 3002,
options: ChannelOptions(
credentials: const ChannelCredentials.insecure(),
codecRegistry: CodecRegistry(codecs: const [GzipCodec(), IdentityCodec()]),
),
);
# PRESENTING CODE
Generating the client
protoc --dart_out=grpc:lib/src/generated -Iprotos protos/eliza.proto
# PRESENTING CODE
Wiring up the service
import 'package:connect_app/shared/api/api.dart';
import 'package:connect_app/src/generated/eliza.pbgrpc.dart';
import 'package:riverpod/riverpod.dart';
final elizaServiceClientProvider = Provider<ElizaServiceClient>((ref) {
return ElizaServiceClient(ref.watch(clientChannelProvider));
});
# PRESENTING CODE
Getting the books
import 'package:connect_app/shared/eliza/eliza_service.dart';
import 'package:connect_app/src/generated/eliza.pbgrpc.dart';
import 'package:riverpod/riverpod.dart';
final booksProvider = FutureProvider<BooksResponse>((ref) async {
return ref
.watch(elizaServiceClientProvider)
.books(Empty())
.timeout(const Duration(seconds: 5));
});
# PRESENTING CODE
Wiring up the UI
import 'package:connect_app/entities/book/model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: MainApp()));
}
class MainApp extends ConsumerWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final booksAsync = ref.watch(booksProvider);
return MaterialApp(
home: Scaffold(
body: Center(
child: booksAsync.when(
data: (data) => Text(data.books.length.toString()),
error: (e, __) => ErrorWidget(e),
loading: () => const CircularProgressIndicator(),
),
),
),
);
}
}
# PRESENTING CODE
Things left to discover
- Deploying a gRPC service
-
Middleware (authentication, logging, etc.)
-
Using Connect client libraries
Links
Getting Buff
By Ryan Edge
Getting Buff
- 38