All posts

The first autogenerated SDK with gRPC and REST

TL;DR: We built the world’s first generated, idiomatic gRPC and REST SDK. If you have public gRPC and REST endpoints, Fern is the SDK provider for you. Schedule a call to learn more!

We’re excited to announce that Fern has launched gRPC and REST in the same SDK. Fern partnered with Pinecone to deliver a best-in-class C# SDK, so that they could better support the .NET community. Pinecone’s official C# SDK is now used and integrated into Microsoft’s Semantic Kernel, which allows developers to interface with a variety of popular vector databases using a common set of abstractions. The SDK supports gRPC for all of Pinecone’s data plane, and traditional REST for the control plane.

Background

Until now, Fern has been laser-focused on consumer-facing REST SDKs. The vast majority of the world is built on REST APIs, but anyone who developed a REST API knows that there aren’t any real standards, and it’s hard to get right. However, with the right developer tooling, we believe that REST APIs can provide the ideal developer experience. So far this approach has proven successful - companies like Cohere, ElevenLabs, Merge, and Webflow use Fern to power their developer platform, and are reaching more developers than ever before.

REST is Fern’s primary focus, but we recognize that companies may need to lean into more performance sensitive technology to fulfill their customers’ needs. Enter gRPC and Protobuf — a battle-tested pair of technology originally open-sourced by Google. gRPC is based on Google's internal RPC framework, Stubby, and many of the world's largest companies, including Uber, Square, Spotify, and Netflix use gRPC and Protobuf in their systems in order to keep up with user demand.

By integrating gRPC alongside REST in our latest SDK, we're bridging the gap between broad compatibility and peak performance, offering developers the best of both worlds.

Pinecone’s use case

Pinecone’s SDK is split into two halves: the control plane and the data plane. The control plane represents the APIs that manage your Pinecone indexes, which take the form of typical CRUD endpoints. These APIs support REST so that it’s easy for users to interact with them in a variety of contexts, such as cURL, Postman, etc.

The data plane represents the APIs that are used to insert and update vectors to your Pinecone index. These operations are far more performance sensitive, so Pinecone appropriately utilizes gRPC to satisfy their requirements. However, this presents a significant challenge - Pinecone needs to support a single SDK that support both HTTP/JSON and gRPC/Protobuf. Until Fern came along, these libraries were manually maintained by Pinecone’s engineering team. In fact, they had to staff an entire SDK team focused on this problem in particular.

Pinecone manually built out support for this use case in Python and Java, but the experience isn’t consistent across their entire SDK offering. For example, they never built out gRPC support in TypeScript, which has been a long-running issue.

Why not just use gRPC directly?

gRPC supports a wide range of code generators, but they are notoriously difficult to incorporate into your technical stack. Even if you manage to get gRPC up and running on your server, it’s a completely different story to produce customer-facing SDKs that are easy to use.

Many of gRPC’s client libraries are not idiomatic, and they often reinvent their own HTTP/2 implementation rather than using the language’s standard library. This makes it difficult for developers to integrate because it’s incompatible with the rest of the language’s ecosystem, including third party packages. Even if you accept these tradeoffs, this approach only works if your customers exclusively use gRPC, which is far less common and approachable than REST.

Plus, interacting with the produced Protobuf types feels like you’re interacting with generated code rather than code tailored by a human. For example, consider the traditional gRPC UX below:

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Pinecone.Grpc;

// Instantiate Pinecone's data plane client.
var channel = GrpcChannel.ForAddress("https://serverless-index.pinecone.io");
var dataPlane = new VectorService.VectorServiceClient(channel);

// Construct gRPC metadata to authenticate every API call.
var metadata = new Metadata();
metadata.Add("Api-Key", "<PINECONE_API_KEY>");

// Create call options to include the gRPC metadata.
var options = new CallOptions(metadata);

// Call the API and retrieve the response.
var call = dataPlane.DescribeIndexStatsAsync(
  new DescribeIndexStatsRequest
  {
      Filter = new Struct
      {
          Fields = 
          {
              { "namespace", Value.ForString("test") }
          }
    }
  },
  options
);
var response = await call.ConfigureAwait(false);

As a developer, I typically go-to-definition to better understand the data structure I’m using. If you go-to-definition for the Protobuf-generated DescribeIndexRequest, you’re presented with the following mess:

/// <summary>
/// The request for the `describe_index_stats` operation.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class DescribeIndexStatsRequest : pb::IMessage<DescribeIndexStatsRequest>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
    , pb::IBufferMessage
#endif
{
  private static readonly pb::MessageParser<DescribeIndexStatsRequest> _parser = new pb::MessageParser<DescribeIndexStatsRequest>(() => new DescribeIndexStatsRequest());
  private pb::UnknownFieldSet _unknownFields;
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public static pb::MessageParser<DescribeIndexStatsRequest> Parser { get { return _parser; } }
    
  ...

  /// <summary>Field number for the "filter" field.</summary>
  public const int FilterFieldNumber = 1;
  private global::Google.Protobuf.WellKnownTypes.Struct filter_;
  /// <summary>
  /// If this parameter is present, the operation only returns statistics
  /// for vectors that satisfy the filter.
  /// See https://docs.pinecone.io/guides/data/filtering-with-metadata.
  /// </summary>
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public global::Google.Protobuf.WellKnownTypes.Struct Filter {
    get { return filter_; }
    set {
      filter_ = value;
    }
  }
  
  ...
}

A lot of the code here has nothing to do with the user-facing API at all. There’s a bunch of noise that makes it harder for the developer to know exactly what they need to do to get their job done.

In summary, there’s a couple things wrong with this approach:

  1. gRPC requires a lot of error prone code for a single API call.

    • ≥ 10 lines of code

    • It’s unclear how to resolve the base URL, configure the API key, or set gRPC call options.

  2. The generated Protobuf types are difficult to grok.

Fern SDKs, on the other hand, are built with simplicity, code readability and IntelliSense at top of mind. When we saw the full gRPC experience for ourselves, we knew Fern could do better.

How it works

Pinecone defines their control plane with OpenAPI and their data plane with Protobuf. Fern supports these different input types seamlessly alongside each other with a SDK configuration that looks like the following:

# generators.yml

api:
  - openapi: ./control-plane/openapi.yaml
  - proto: ./data-plane/data_plane.proto

groups:
  dotnet-sdk:
    generators:
      - name: fernapi/fern-csharp-sdk
        version: 1.4.0
        github:
          repository: pinecone-io/pinecone-dotnet-client
        output:
          location: nuget
          package-name: Pinecone.Client
          api-key: ${NUGET_API_KEY}

With this, any API updates (across both OpenAPI and Protobuf) are immediately reflected in their generated SDKs and published to the configured package registry (i.e. NuGet for C#).

Fern offers a variety of other features over traditional gRPC code generators including:

  • Default base URL

  • Auto pagination

  • Strongly typed errors

  • OAuth

  • Generated examples

  • Code snippets

  • Unified client

  • and more

Pinecone’s C# SDK

"By partnering with Fern, we were able to deliver a high-quality C# SDK quickly"

— Bear Douglas, Director of Developer Relations at Pinecone

Pinecone’s Fern-generated SDK is hosted on GitHub at pinecone-io/pinecone-dotnet-client. Upon visiting the landing page, you’re presented with a clean README.md that outlines all of the SDK’s features.

In the README.md, you will notice an example for the same DescribeIndexStats endpoint mentioned above. You can adapt this snippet to use Fern’s generated SDK like so:

using Pinecone;

// Instantiate Pinecone's client.
var pinecone = new PineconeClient();

// Create a gRPC connection to the Pinecone index named 'serverless-index'.
var index = pinecone.Index("serverless-index");

// Call the API and retrieve the response.
var response = await index.DescribeIndexStatsAsync(
  new DescribeIndexStatsRequest
  {
      Filter = new Metadata()
      {
          { "namespace", "test" }
      }
  }
);

The Api-Key header is automatically read from the PINECONE_API_KEY environment variable, so no additional client constructor configuration is required. When you go-to-definition you’re presented with an idiomatic, easy to read class:

using Proto = Pinecone.Grpc;

namespace Pinecone;

public record DescribeIndexStatsRequest
{
    /// <summary>
    /// If this parameter is present, the operation only returns statistics
    /// for vectors that satisfy the filter.
    /// See https://docs.pinecone.io/guides/data/filtering-with-metadata.
    /// </summary>
    [JsonPropertyName("filter")]
    public Metadata? Filter { get; set; }

    public override string ToString()
    {
        return JsonUtils.Serialize(this);
    }
    
    /// <summary>
    /// Maps the DescribeIndexStatsRequest type into its Protobuf-equivalent representation.
    /// </summary>
    internal Proto.DescribeIndexStatsRequest ToProto()
    {
        var result = new Proto.DescribeIndexStatsRequest();
        if (Filter != null)
        {
            result.Filter = Filter.ToProto();
        }
        return result;
    }
}

As you can see, the Fern-generated type maps itself to the Protobuf-generated type to handle all the Protobuf serialization logic. Fern doesn’t need to reinvent the wheel here - the SDK can continue to use the performant, Google-maintained Protobuf serialization code, but provide a much cleaner interface on top of it.

Automatic updates

Now that Pinecone is set up with Fern, adding new functionality is as simple as updating their API specifications. For example, suppose Pinecone wanted to add another gRPC endpoint like the following:

// data_plane.proto

message ClearIndexRequest {
  string namespace = 1;
}

message ClearIndexResponse {
  // The number of vectors cleared, if any.
  uint32 vector_count = 1;
}

service VectorService {
  // Clears the index of all vectors.
  rpc ClearIndex(ClearIndexRequest) returns (ClearIndexResponse);  
}

Pinecone can simply run fern generate and after a few seconds Fern will open a PR like so:

$ fern generate --group csharp-sdk
...
Generating src/ClearIndexRequest.cs
Generating src/ClearIndexResponse.cs
...
Pushed branch: https://github.com/pinecone-io/pinecone-dotneet-sdk/tree/fern-bot/09-29-2024-1038PM
┌─
fernapi/fern-csharp-sdk
└─

Now if you navigate to the repository’s PR tab, you’ll notice the following:

As you can see, the PR verifies that the code compiles and all tests continue to pass. If you take a closer look at the updated code, you’ll notice the new ClearIndexAsync method:

Notice that the original Protobuf comments are preserved, usage examples are included in the method documentation, and all exceptions are mapped into an SDK-specific exception. Zero lines of additional hand-written code is required.

The Pinecone team can simply merge the PR and the latest version will be automatically published to NuGet. Pinecone’s users can start using the latest version by using the dotnet command, and everything just works.

Acknowledgements

The Protobuf space has been hugely impacted and improved by Buf. Fern was able to leverage the buf CLI, which provides a far better Protobuf experience than working with Google’s original Protobuf compiler, protoc. If your company defines any Protobuf APIs, make sure to check out Buf’s Schema Registry (BSR).

Wrapping up

Fern’s gRPC support breaks new ground for the gRPC community. The generated gRPC SDK is idiomatic and equipped with a variety of significant quality of life features like a default base URL, configurable environments (e.g. production vs. staging), and automatic API key configuration. Better yet, Fern is able to produce code examples and snippets for all of your gRPC endpoints, which are generated straight into the SDK itself.

Talk to us

We’re thrilled to start serving the gRPC community and look forward to additional language support in the future. Schedule a call to learn more and get started!

Reading time

5 to 10 minutes

Industry

Infrastructure

Fern Stack

SDKs

Date

Oct 7, 2024

Author

Alex McKinney

Time to value

1 month

All posts

The first autogenerated SDK with gRPC and REST

The first autogenerated SDK with gRPC and REST

TL;DR: We built the world’s first generated, idiomatic gRPC and REST SDK. If you have public gRPC and REST endpoints, Fern is the SDK provider for you. Schedule a call to learn more!

We’re excited to announce that Fern has launched gRPC and REST in the same SDK. Fern partnered with Pinecone to deliver a best-in-class C# SDK, so that they could better support the .NET community. Pinecone’s official C# SDK is now used and integrated into Microsoft’s Semantic Kernel, which allows developers to interface with a variety of popular vector databases using a common set of abstractions. The SDK supports gRPC for all of Pinecone’s data plane, and traditional REST for the control plane.

Background

Until now, Fern has been laser-focused on consumer-facing REST SDKs. The vast majority of the world is built on REST APIs, but anyone who developed a REST API knows that there aren’t any real standards, and it’s hard to get right. However, with the right developer tooling, we believe that REST APIs can provide the ideal developer experience. So far this approach has proven successful - companies like Cohere, ElevenLabs, Merge, and Webflow use Fern to power their developer platform, and are reaching more developers than ever before.

REST is Fern’s primary focus, but we recognize that companies may need to lean into more performance sensitive technology to fulfill their customers’ needs. Enter gRPC and Protobuf — a battle-tested pair of technology originally open-sourced by Google. gRPC is based on Google's internal RPC framework, Stubby, and many of the world's largest companies, including Uber, Square, Spotify, and Netflix use gRPC and Protobuf in their systems in order to keep up with user demand.

By integrating gRPC alongside REST in our latest SDK, we're bridging the gap between broad compatibility and peak performance, offering developers the best of both worlds.

Pinecone’s use case

Pinecone’s SDK is split into two halves: the control plane and the data plane. The control plane represents the APIs that manage your Pinecone indexes, which take the form of typical CRUD endpoints. These APIs support REST so that it’s easy for users to interact with them in a variety of contexts, such as cURL, Postman, etc.

The data plane represents the APIs that are used to insert and update vectors to your Pinecone index. These operations are far more performance sensitive, so Pinecone appropriately utilizes gRPC to satisfy their requirements. However, this presents a significant challenge - Pinecone needs to support a single SDK that support both HTTP/JSON and gRPC/Protobuf. Until Fern came along, these libraries were manually maintained by Pinecone’s engineering team. In fact, they had to staff an entire SDK team focused on this problem in particular.

Pinecone manually built out support for this use case in Python and Java, but the experience isn’t consistent across their entire SDK offering. For example, they never built out gRPC support in TypeScript, which has been a long-running issue.

Why not just use gRPC directly?

gRPC supports a wide range of code generators, but they are notoriously difficult to incorporate into your technical stack. Even if you manage to get gRPC up and running on your server, it’s a completely different story to produce customer-facing SDKs that are easy to use.

Many of gRPC’s client libraries are not idiomatic, and they often reinvent their own HTTP/2 implementation rather than using the language’s standard library. This makes it difficult for developers to integrate because it’s incompatible with the rest of the language’s ecosystem, including third party packages. Even if you accept these tradeoffs, this approach only works if your customers exclusively use gRPC, which is far less common and approachable than REST.

Plus, interacting with the produced Protobuf types feels like you’re interacting with generated code rather than code tailored by a human. For example, consider the traditional gRPC UX below:

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Pinecone.Grpc;

// Instantiate Pinecone's data plane client.
var channel = GrpcChannel.ForAddress("https://serverless-index.pinecone.io");
var dataPlane = new VectorService.VectorServiceClient(channel);

// Construct gRPC metadata to authenticate every API call.
var metadata = new Metadata();
metadata.Add("Api-Key", "<PINECONE_API_KEY>");

// Create call options to include the gRPC metadata.
var options = new CallOptions(metadata);

// Call the API and retrieve the response.
var call = dataPlane.DescribeIndexStatsAsync(
  new DescribeIndexStatsRequest
  {
      Filter = new Struct
      {
          Fields = 
          {
              { "namespace", Value.ForString("test") }
          }
    }
  },
  options
);
var response = await call.ConfigureAwait(false);

As a developer, I typically go-to-definition to better understand the data structure I’m using. If you go-to-definition for the Protobuf-generated DescribeIndexRequest, you’re presented with the following mess:

/// <summary>
/// The request for the `describe_index_stats` operation.
/// </summary>
[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]
public sealed partial class DescribeIndexStatsRequest : pb::IMessage<DescribeIndexStatsRequest>
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
    , pb::IBufferMessage
#endif
{
  private static readonly pb::MessageParser<DescribeIndexStatsRequest> _parser = new pb::MessageParser<DescribeIndexStatsRequest>(() => new DescribeIndexStatsRequest());
  private pb::UnknownFieldSet _unknownFields;
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public static pb::MessageParser<DescribeIndexStatsRequest> Parser { get { return _parser; } }
    
  ...

  /// <summary>Field number for the "filter" field.</summary>
  public const int FilterFieldNumber = 1;
  private global::Google.Protobuf.WellKnownTypes.Struct filter_;
  /// <summary>
  /// If this parameter is present, the operation only returns statistics
  /// for vectors that satisfy the filter.
  /// See https://docs.pinecone.io/guides/data/filtering-with-metadata.
  /// </summary>
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public global::Google.Protobuf.WellKnownTypes.Struct Filter {
    get { return filter_; }
    set {
      filter_ = value;
    }
  }
  
  ...
}

A lot of the code here has nothing to do with the user-facing API at all. There’s a bunch of noise that makes it harder for the developer to know exactly what they need to do to get their job done.

In summary, there’s a couple things wrong with this approach:

  1. gRPC requires a lot of error prone code for a single API call.

    • ≥ 10 lines of code

    • It’s unclear how to resolve the base URL, configure the API key, or set gRPC call options.

  2. The generated Protobuf types are difficult to grok.

Fern SDKs, on the other hand, are built with simplicity, code readability and IntelliSense at top of mind. When we saw the full gRPC experience for ourselves, we knew Fern could do better.

How it works

Pinecone defines their control plane with OpenAPI and their data plane with Protobuf. Fern supports these different input types seamlessly alongside each other with a SDK configuration that looks like the following:

# generators.yml

api:
  - openapi: ./control-plane/openapi.yaml
  - proto: ./data-plane/data_plane.proto

groups:
  dotnet-sdk:
    generators:
      - name: fernapi/fern-csharp-sdk
        version: 1.4.0
        github:
          repository: pinecone-io/pinecone-dotnet-client
        output:
          location: nuget
          package-name: Pinecone.Client
          api-key: ${NUGET_API_KEY}

With this, any API updates (across both OpenAPI and Protobuf) are immediately reflected in their generated SDKs and published to the configured package registry (i.e. NuGet for C#).

Fern offers a variety of other features over traditional gRPC code generators including:

  • Default base URL

  • Auto pagination

  • Strongly typed errors

  • OAuth

  • Generated examples

  • Code snippets

  • Unified client

  • and more

Pinecone’s C# SDK

"By partnering with Fern, we were able to deliver a high-quality C# SDK quickly"

— Bear Douglas, Director of Developer Relations at Pinecone

Pinecone’s Fern-generated SDK is hosted on GitHub at pinecone-io/pinecone-dotnet-client. Upon visiting the landing page, you’re presented with a clean README.md that outlines all of the SDK’s features.

In the README.md, you will notice an example for the same DescribeIndexStats endpoint mentioned above. You can adapt this snippet to use Fern’s generated SDK like so:

using Pinecone;

// Instantiate Pinecone's client.
var pinecone = new PineconeClient();

// Create a gRPC connection to the Pinecone index named 'serverless-index'.
var index = pinecone.Index("serverless-index");

// Call the API and retrieve the response.
var response = await index.DescribeIndexStatsAsync(
  new DescribeIndexStatsRequest
  {
      Filter = new Metadata()
      {
          { "namespace", "test" }
      }
  }
);

The Api-Key header is automatically read from the PINECONE_API_KEY environment variable, so no additional client constructor configuration is required. When you go-to-definition you’re presented with an idiomatic, easy to read class:

using Proto = Pinecone.Grpc;

namespace Pinecone;

public record DescribeIndexStatsRequest
{
    /// <summary>
    /// If this parameter is present, the operation only returns statistics
    /// for vectors that satisfy the filter.
    /// See https://docs.pinecone.io/guides/data/filtering-with-metadata.
    /// </summary>
    [JsonPropertyName("filter")]
    public Metadata? Filter { get; set; }

    public override string ToString()
    {
        return JsonUtils.Serialize(this);
    }
    
    /// <summary>
    /// Maps the DescribeIndexStatsRequest type into its Protobuf-equivalent representation.
    /// </summary>
    internal Proto.DescribeIndexStatsRequest ToProto()
    {
        var result = new Proto.DescribeIndexStatsRequest();
        if (Filter != null)
        {
            result.Filter = Filter.ToProto();
        }
        return result;
    }
}

As you can see, the Fern-generated type maps itself to the Protobuf-generated type to handle all the Protobuf serialization logic. Fern doesn’t need to reinvent the wheel here - the SDK can continue to use the performant, Google-maintained Protobuf serialization code, but provide a much cleaner interface on top of it.

Automatic updates

Now that Pinecone is set up with Fern, adding new functionality is as simple as updating their API specifications. For example, suppose Pinecone wanted to add another gRPC endpoint like the following:

// data_plane.proto

message ClearIndexRequest {
  string namespace = 1;
}

message ClearIndexResponse {
  // The number of vectors cleared, if any.
  uint32 vector_count = 1;
}

service VectorService {
  // Clears the index of all vectors.
  rpc ClearIndex(ClearIndexRequest) returns (ClearIndexResponse);  
}

Pinecone can simply run fern generate and after a few seconds Fern will open a PR like so:

$ fern generate --group csharp-sdk
...
Generating src/ClearIndexRequest.cs
Generating src/ClearIndexResponse.cs
...
Pushed branch: https://github.com/pinecone-io/pinecone-dotneet-sdk/tree/fern-bot/09-29-2024-1038PM
┌─
fernapi/fern-csharp-sdk
└─

Now if you navigate to the repository’s PR tab, you’ll notice the following:

As you can see, the PR verifies that the code compiles and all tests continue to pass. If you take a closer look at the updated code, you’ll notice the new ClearIndexAsync method:

Notice that the original Protobuf comments are preserved, usage examples are included in the method documentation, and all exceptions are mapped into an SDK-specific exception. Zero lines of additional hand-written code is required.

The Pinecone team can simply merge the PR and the latest version will be automatically published to NuGet. Pinecone’s users can start using the latest version by using the dotnet command, and everything just works.

Acknowledgements

The Protobuf space has been hugely impacted and improved by Buf. Fern was able to leverage the buf CLI, which provides a far better Protobuf experience than working with Google’s original Protobuf compiler, protoc. If your company defines any Protobuf APIs, make sure to check out Buf’s Schema Registry (BSR).

Wrapping up

Fern’s gRPC support breaks new ground for the gRPC community. The generated gRPC SDK is idiomatic and equipped with a variety of significant quality of life features like a default base URL, configurable environments (e.g. production vs. staging), and automatic API key configuration. Better yet, Fern is able to produce code examples and snippets for all of your gRPC endpoints, which are generated straight into the SDK itself.

Talk to us

We’re thrilled to start serving the gRPC community and look forward to additional language support in the future. Schedule a call to learn more and get started!

Reading time

5 to 10 minutes

Industry

Infrastructure

Fern Stack

SDKs

Date

Oct 7, 2024

Author

Alex McKinney

Time to value

1 month