Modernizing APIs with gRPC
2007 - Facebook Query Language created
2012 - GraphQL created internally as FQL successor
2015 - Facebook announces Relay
2019 and beyond - GraphQL eats the world
@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
GraphQL is an alternative to REST for developing APIs, not a replacement.
A high performance, open source universal RPC framework
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
Connecting mobile devices, browser clients to backend services
mkdir connect-api
cd connect-api
npm init -y
npm install typescript tsx
npx tsc --init
# PRESENTING CODE
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
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
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
npx buf generate proto
# PRESENTING CODE
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
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
dart pub add riverpod flutter_riverpod grpc protobuf
# PRESENTING CODE
import 'package:grpc/grpc_web.dart';
final api = GrpcWebClientChannel.xhr(Uri.parse('https://localhost:3001'));
# PRESENTING CODE
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
protoc --dart_out=grpc:lib/src/generated -Iprotos protos/eliza.proto
# PRESENTING CODE
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
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
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
Middleware (authentication, logging, etc.)
Using Connect client libraries