{Getting Buff}

Modernizing APIs with gRPC

Agenda

  1. Brief history of GraphQL
  2. GraphQL pros & cons
  3. Introduction to gRPC
  4. Brief history of gRPC
  5. GraphQL pros & cons
  6. A simple code example
  7. 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