# Connect v1 to v2 migration guide Connect v2 provides new features and simplifies some common APIs. In addition, it makes use of all the [enhancements of Protobuf-ES v2](https://buf.build/blog/protobuf-es-v2). If you're currently using Connect v1, this document walks you through all you need to know to migrate and start using it right away. > [!IMPORTANT] > > - Node 16 is no longer supported. Connect v2 now supports Node versions **18.14.1** and up. > - TypeScript 4.1 is no longer supported. Connect v2 now requires at least TypeScript **v4.9.5**. ## Running the migration tool To help with the process of migrating, we provide a tool called [@connectrpc/connect-migrate](https://www.npmjs.com/package/@connectrpc/connect-migrate) which will take care of dependency and plugin updates as well as a few minor code changes. As a first step, execute the following command: ```shellsession npx @connectrpc/connect-migrate@latest ``` While the tool will do a lot of the dependency legwork for you, there are many use cases in code that it does not cover. Please read on to understand what has changed and how you can leverage new features your application. > [!TIP] > > If you use generated SDKs, read the section [Upgrading generated SDKs](#upgrading-generated-sdks) > before you run `connect-migrate`. ## Dependency updates in package.json One important dependency change to be aware of is that the plugin `protoc-gen-connect-es` has been removed in v2. Connect now relies on service descriptors generated by the Protobuf-ES v2 plugin `protoc-gen-es` and no longer generates code itself. Therefore, that dependency must be removed entirely from `package.json`. Your mileage may vary according to what `@bufbuild` and `@connectrpc` packages you depend on, but a list of relevant, compatible dependencies should look similar to the following: ```diff "dependencies": { "@bufbuild/protobuf": "^2.2.0", "@bufbuild/protoc-gen-es": "^2.2.0", "@connectrpc/connect": "^2.0.0", - "@connectrpc/protoc-gen-connect-es": "^1.0.0", "@connectrpc/connect-web": "^2.0.0", "@connectrpc/connect-node": "^2.0.0", "@connectrpc/connect-next": "^2.0.0", "@connectrpc/connect-fastify": "^2.0.0", "@connectrpc/connect-express": "^2.0.0", "@connectrpc/connect-query": "^2.0.0", "@connectrpc/protoc-gen-connect-query": "^2.0.0", "@connectrpc/connect-playwright": "^0.6.0" } ``` :white_check_mark: The `connect-migrate` tool will handle this. ## Update buf.gen.yaml Remove any usage of the `protoc-gen-connect-es` plugin from `buf.gen.yaml`. If you are using local plugins: ```diff # buf.gen.yaml version: v2 plugins: - local: protoc-gen-es out: src/gen include_imports: true opt: target=ts - - local: protoc-gen-connect-es - out: src/gen - opt: target=ts ``` If you are using remote plugins: ```diff # buf.gen.yaml version: v2 plugins: - - remote: buf.build/bufbuild/es:v1.10.0 + - remote: buf.build/bufbuild/es:v2.2.0 out: src/gen include_imports: true opt: target=ts - - remote: buf.build/connectrpc/es - out: src/gen - opt: target=ts ``` :white_check_mark: The `connect-migrate` tool will handle this. #### Remove unused files with `clean` The `*_connect.ts` files generated by the old plugin are no longer needed. We recommend to use the `clean` option provided by the Buf CLI introduced in v1.36.0: ```diff # buf.gen.yaml version: v2 + clean: true plugins: - local: protoc-gen-es out: src/gen include_imports: true ``` With this option, `buf generate` will delete the contents of `src/gen` before generating code. #### Update plugin options The plugin option `import_extension` has changed behavior: If you are using [Node16 module resolution](https://www.typescriptlang.org/tsconfig/#moduleResolution) and need the `.js` extension on all import paths, add the plugin option `import_extension=js`: ```diff # buf.gen.yaml version: v2 plugins: - local: protoc-gen-es out: src/gen include_imports: true opt: - target=ts + - import_extension=js ``` If you don't want the `.js` extension added to import paths, you can remove the plugin option `import_extension=none` - it's the default behavior now: ```diff # buf.gen.yaml version: v2 plugins: - local: protoc-gen-es out: src/gen include_imports: true opt: - target=ts - - import_extension=none ``` If you have been using the plugin option `ts_nocheck=false`, you can remove it as well - it's the default behavior now. ## Re-generate code Now that dependencies and `buf.gen.yaml` are updated, the next step is to re-generate code. The migration tool does not handle code generation, so be sure to do so in whatever way your project is configured. For example, `npx buf generate` or `npm run generate`. > [!NOTE] > Ensure that your `buf.gen.yaml` includes the following options to generate > code for imports. > > - `include_imports: true` See the [Gotchas](#the-new-plugins-generates-missing-imports) section for an explanation. ## Update your application code Now that dependencies are updated and new code is generated, let's go through the changes to your application code. ### Import paths Once your code is generated and the vestigial `*_connect.ts` files are removed, import paths will need to be updated. This is usually an update from `*_connect` to `*_pb`: ```diff - import { ElizaService } from "./gen/eliza_connect"; + import { ElizaService } from "./gen/eliza_pb"; ``` :white_check_mark: The `connect-migrate` tool will handle this. ### New APIs for Protobuf messages In many applications, it's likely that you'll encounter the most significant change in version 2.0 of Protobuf-ES: we no longer generate classes for Protobuf messages. Instead, we generate a schema object and an associated TypeScript type definition for each message. Instead of the `new` keyword, you create a message with a function call: ```diff - import { SayRequest } from "./gen/eliza_connect.js"; + import { SayRequestSchema } from "./gen/eliza_pb.js"; + import { create } from "@bufbuild/protobuf"; - const sayRequest = new SayRequest({ + const sayRequest = create(SayRequestSchema, { sentence: "Hello", }); ``` :white_check_mark: The `connect-migrate` tool will handle this. Messages are now plain TypeScript types, which greatly improves compatibility with the ecosystem. For example, messages can be passed from a server-side component in Next.js to a client-side component without losing any data or types. #### Message class methods Because messages no longer have attached class methods, a standalone function is provided as a replacement. Here is an example for `toBinary`: ```diff import { toBinary } from "@bufbuild/protobuf"; import { SayRequestSchema } from "./gen/eliza_pb"; - sayRequest.toBinary(); + toBinary(SayRequestSchema, sayRequest); ``` The same applies to the methods `equals`, `clone`, `toJson`, and `toJsonString`, and to the static class methods. :white_check_mark: The `connect-migrate` tool will handle the static class methods `fromBinary`, `fromJson`, and `fromJsonString`. > [!WARNING] > > Note that messages no longer implement the magic `toJSON` method, which serializes > a message with the Protobuf JSON format when it's passed to `JSON.stringify`. Make > sure to always serializes to JSON with the `toJson` or `toJsonString` function. #### Identifying messages Messages no longer include a reference to their type, but they have a property for their qualified Protobuf name: ```diff - sayRequest.getType().typeName; // "connectrpc.eliza.v1.SayRequest" + sayRequest.$typeName; // "connectrpc.eliza.v1.SayRequest" ``` To identify an unknown message, the function `isMessage` is still supported: ```diff - import { SayRequest } from "./gen/eliza_connect.js"; + import { SayRequestSchema } from "./gen/eliza_pb.js"; import { isMessage } from "@bufbuild/protobuf"; - if (isMessage(x, SayRequest)) { + if (isMessage(x, SayRequestSchema)) { x.sentence; } ``` :white_check_mark: The `connect-migrate` tool will handle `isMessage` calls. #### PlainMessage removed The `PlainMessage<T>` type was used to represent just the fields of a message, without class methods. Now that messages are plain types, the type is no longer necessary and has been removed, along with the function `toPlainMessage`. In most cases, you can simply remove usage of `PlainMessage`: ```diff - import type { PlainMessage } from "@bufbuild/protobuf"; import type { SayRequest } from "./gen/eliza_pb"; - const sayRequest: PlainMessage<SayReqest> = { + const sayRequest: SayReqest = { $typeName: "connectrpc.eliza.v1.SayRequest", sentence: "Hello", }; ``` The `$typeName` property is required to identify messages. If you have a use-case where it's distracting, use built-in types to remove it: ```ts const sayRequest: Omit<SayReqest, "$typeName"> = { sentence: "Hello", }; ``` > [!TIP] > > `proto3` messages are plain objects, but with `proto2` or Editions, messages > may use the prototype chain to track field presence. See the section > [Field presence and default values](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#field-presence-and-default-values) > in the Protobuf-ES documentation. > If you need messages to be as simple as possible in all cases, see the section > about [JSON types](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#json-types). #### PartialMessage removed Similar to `PlainMessage`, `PartialMessage` has been removed. It was used for partial initializer objects when creating new messages. For this use case, the replacement is `MessageInitShape`. It retrieves the type from a descriptor for forwards compatibility. For other use cases, built-in types are preferred, now that messages are plain types. #### proto2 field changes `proto2` fields with default values are no longer generated as optional properties: ```diff /** * @generated from field: required int32 num = 3; */ - num?: number; + num: number; ``` > [!TIP] > > In general, this makes working with `proto2` messages much more convenient, because > you no longer have to handle `undefined` when accessing the property. You can rely > on the default value `0` instead. If you need to distinguish between an absent > value for a field and the default value, use the function `isFieldSet` from > `@bufbuild/protobuf`. You can learn more in the section > [Field presence and default values](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#field-presence-and-default-values) > in the Protobuf-ES documentation. #### Struct field changes The well-known type `google.protobuf.Struct` is now generated as a more-convenient `JsonObject` when used as a message field: ```diff /** * @generated from field: google.protobuf.Struct struct = 1; */ - struct?: Struct; + struct?: JsonObject; ``` > [!TIP] > > This feature makes it very easy to work with `Struct` fields: > > ```ts > myMessage.struct = { > text: "abc", > number: 123, > }; > ``` #### Well-known types All well-known types have been moved to the subpath export `@bufbuild/protobuf/wkt`. For example, if you want to refer to `google.protobuf.Timestamp`: ```diff - import type { Timestamp } from "@bufbuild/protobuf"; + import type { Timestamp } from "@bufbuild/protobuf/wkt"; ``` :white_check_mark: The `connect-migrate` tool will handle this. Helpers that were previously part of the generated class are now standalone functions, also exported from `@bufbuild/protobuf/wkt`: ```diff - import type { Timestamp } from "@bufbuild/protobuf"; + import type { Timestamp } from "@bufbuild/protobuf/wkt"; + import { timestampDate } from "@bufbuild/protobuf/wkt"; const timestamp: Timestamp = ... - const date: Date = timestamp.toDate(); + const date: Date = timestampDate(timestamp); ``` #### Reflection changes Reflection has received a major update in Protobuf-ES v2, and is much more capable and flexible now. Instead of providing minimal field information from generated code, full Protobuf descriptors are available. Here is an example for finding the JSON name of a field: ```diff - SayRequest.fields.list() + SayRequestSchema.fields .find((f) => f.localName === "sentence") ?.jsonName; ``` All common use cases continue to be supported, but types and properties have changed to consolidate the API and provide new features: | Old type | Replacement | | ------------- | ------------------------------------------------------------------------------------------ | | `MessageType` | `DescMessage` | | `EnumType` | `DescEnum` | | `FieldInfo` | `DescField` | | `OneofInfo` | `DescOneof` | | `ServiceType` | `DescService` - also see [Changes to service descriptors](#changes-to-service-descriptors) | | `MethodInfo` | `DescMethod` | > [!TIP] > > For more information, see the [Protobuf reflection documentation](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#reflection). #### Registries Registries are commonly used for serializing the well-known type `google.protobuf.Any`. To create a registry, you simply pass the schema objects instead of message classes: ```diff import { createRegistry } from "@bufbuild/protobuf"; - import { SayRequest, SayResponse } from "./gen/eliza_pb"; + import { SayRequestSchema, SayRequestSchema } from "./gen/eliza_pb"; const registry = createRegistry( - SayRequest, SayResponse, + SayRequestSchema, SayResponseSchema, ); ``` > [!TIP] > In the new version, registries also support files, and can be composed or mutated. > See the [documentation on Protobuf registries](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#registries) to learn more. ### `createPromiseClient` is removed Promise clients are now the default and the previously-deprecated function `createPromiseClient` has been removed. Update any call sites using `createPromiseClient` to use `createClient`. ```diff - import { createPromiseClient } from "@connectrpc/connect-node"; + import { createClient } from "@connectrpc/connect-node"; - createPromiseClient(ElizaService, transport); + createClient(ElizaService, transport); ``` :white_check_mark: The `connect-migrate` tool will handle this. ### gRPC transport requires HTTP/2 The gRPC Transport now requires HTTP/2. If you are using `createGrpcTransport` and specifying an `httpVersion`, it will fail compilation. Remove the `httpVersion` property to use the default of HTTP/2. ```diff import { createGrpcTransport } from "@connectrpc/connect-node"; createGrpcTransport({ baseUrl: "https://demo.connectrpc.com", - httpVersion: "2", }); ``` Note that if you were relying on HTTP/1.1 as part of your gRPC strategy, this may require bigger architectural changes, but the hope is that this is not a common problem. ### Transport option `credentials` is removed We have removed the `credentials` option from transports as well as the `init` option in interceptors. These two options were used to customize `fetch` routines. To set the fetch option `credentials`, provide a fetch override: ```diff createConnectTransport({ baseUrl: "/", - credentials: "include", + fetch: (input, init) => fetch(input, { ...init, credentials: "include" }), }); ``` ### JSON option `typeRegistry` renamed JSON serialization options (passed to a transport or a server plugin) have been updated in Protobuf-ES v2. The option `typeRegistry` has been renamed to `registry`: ```diff import { createRegistry } from "@bufbuild/protobuf"; import { createConnectTransport } from "@connectrpc/connect-web"; - import { SayRequest } from "./gen/eliza_pb"; + import { SayRequestSchema } from "./gen/eliza_pb"; const transport = createConnectTransport({ baseUrl: "https://demo.connectrpc.com", jsonOptions: { - typeRegistry: createRegistry(SayRequest), + registry: createRegistry(SayRequestSchema), }, }); ``` > [!TIP] > Registries have received a major update in Protobuf-ES v2 and are > much more capable and flexible now. For more information, see the > [Protobuf registry documentation](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#registries). ### JSON option `emitDefaultValues` renamed JSON serialization options (passed to a transport or a server plugin) have been updated in Protobuf-ES v2. The option `emitDefaultValues` has been renamed to `alwaysEmitImplicit`: ```diff await server.register( fastifyConnectPlugin, { routes, jsonOptions: { - emitDefaultValues: true, + alwaysEmitImplicit: true, }, }, ); ``` > [!TIP] > > When this option is enabled, `proto3` default values such as `0`, `"""`, or `false` > are serialized to JSON, but `proto2` default values are not. ### Changes to service descriptors Connect now relies on service descriptors generated by the Protobuf-ES v2 plugin `protoc-gen-es` and no longer generates code itself. The type for service descriptors changes from `ServiceType` to `DescService` from `@bufbuild/protobuf`. The descriptors still provide the same functionality - typed metadata for clients and servers - but in a slightly different form. Types and properties have changed to consolidate the reflection APIs and to provide new features. > [!TIP] > > Service and method descriptors provide access to custom options now, which can > be very useful in interceptors to control authorization and other details. > See the documentation for [Protobuf custom options](https://github.com/bufbuild/protobuf-es/blob/v2.2.2/MANUAL.md#custom-options) to learn more. #### Access a method by name The `methods` property is renamed to `method`: ```diff - const say = ElizaService.methods.say; + const say = ElizaService.method.say; ``` > [!TIP] > > `methods` provides an Array of the methods. #### Method kind To distinguish between unary and streaming RPCs, use the `methodKind` property: ```diff - import { MethodKind } from "@bufbuild/protobuf"; - say.kind; // MethodKind.Unary + say.methodKind; // "unary" ``` #### Method idempotency The enum `MethodIdempotency` has been replaced by the well-known type `MethodOptions_IdempotencyLevel`: ```diff - import { MethodIdempotency } from "@bufbuild/protobuf"; + import { MethodOptions_IdempotencyLevel } from "@bufbuild/protobuf/wkt"; - say.idempotency; // MethodIdempotency.NoSideEffects + say.idempotency; // MethodOptions_IdempotencyLevel.NoSideEffects ``` ### Changes to interceptors Interceptors for streaming RPCs now use appropriate stream types. In v1, the server used `UnaryRequest` and `StreamResponse` for server-streaming RPCs, while the client always uses streaming variants. This was unintended behavior and has been fixed in v2. Now all streaming RPCs use the `StreamRequest` and `StreamResponse` types on the server as well. The `init` property with fetch options has been removed. As a replacement to determine whether an incoming request is a Connect GET request in server-side interceptors, the property `requestMethod: string` has been added to intercepted requests. This property is symmetrical to `HandlerContext.requestMethod`. ### Service argument removed from `ConnectRouter.rpc` Because method descriptors are now self-sufficient, it is no longer necessary to pass the service descriptor to `ConnectRouter.rpc`, and the argument has been removed from the method signature. Update your call-sites as follows: ```diff const routes = ({rpc}: ConnectRouter) => { - rpc(ElizaService, ElizaService.say, impl); + rpc(ElizaService.say, impl); } ``` > [!NOTE] > > The same change applies to the `Transport` interface. This is only relevant if > you access `Transports` without a client, or if you have implemented your own > custom transport. ### Constructing error details If you raise a Connect error on a server, you can include arbitrary Protobuf messages as error details. The syntax to provide them has changed slightly: In Connect v1, error details were specified as message instances. In v2, error details are now an object that specifies both a message schema and initialization object. For example: ```diff - import { LocalizedMessage } from "./gen/google/rpc/error_details_pb"; - const details = [ - new LocalizedMessage({ - locale: "fr-CH", - message: "Je n'ai plus de mots.", - }), - ]; + import { LocalizedMessageSchema } from "./gen/google/rpc/error_details_pb"; + const details = [ + { + desc: LocalizedMessageSchema, + value: { + locale: "fr-CH", + message: "Je n'ai plus de mots.", + } + }, + ]; const metadata = new Headers({ "words-left": "none" }); throw new ConnectError( "I have no words anymore.", Code.ResourceExhausted, metadata, details, ); ``` ## Upgrading generated SDKs The `connect-migrate` tool does not upgrade generated SDKs at this point in time. We recommend that you use the following steps to upgrade: 1. Uninstall old generated SDKs 2. Run the migration tool 3. Install new generated SDKs 4. Update your application code #### 1. Uninstall old generated SDKs Since the Connect plugin no longer exists in v2, any generated SDK dependencies in your `package.json` that rely on this plugin (i.e. have `connectrpc_es` as part of their name) must be updated to use the Protobuf-ES v2 plugin instead. The name of a generated SDK dependency is structured as follows: ``` @buf/{module_owner}_{module_name}.{plugin_owner}_{plugin_name} ``` For example, if you are using a generated SDK for the BSR module [buf.build/googleapis/googleapis](https://buf.build/googleapis/googleapis), you have dependency on `@buf/googleapis_googleapis.connectrpc_es` in your package.json file. Run the following command to remove the dependency: ```shellsession npm remove @buf/googleapis_googleapis.connectrpc_es ``` To do this for other modules, simply replace `googleapis/googleapis` with your module owner and name. #### 2. Run the migration tool ```shellsession npx @connectrpc/connect-migrate@latest ``` This command will update your dependencies on `@connect` and `@bufbuild` packages, so that you can install the new generated SDK. #### 3. Install new generated SDKs Now you need to replace the old generated SDK with a dependency on `@buf/googleapis_googleapis.bufbuild_es` (same BSR module, but using the [Protobuf-ES v2 plugin](https://buf.build/bufbuild/es)): ```shellsession npm install @buf/googleapis_googleapis.bufbuild_es@latest ``` Your `package.json` should now resemble the following: ```diff "dependencies": { ... - "@buf/googleapis_googleapis.connectrpc_es": "^1.6.1-20241107203341-553fd4b4b3a6.1", + "@buf/googleapis_googleapis.bufbuild_es": "^2.2.2-20241107203341-553fd4b4b3a6.1", "@connectrpc/connect-web": "^2.0.0", "@bufbuild/protobuf": "^2.2.0", ... } ``` > [!NOTE] > Your versions will differ per module. #### 4. Update your application code Now your dependencies are updated, and you can follow the guide to [Update your application code](#update-your-application-code). Make sure to update your imports to the new package name and file names: ```diff - import { ByteStream } from "@buf/googleapis_googleapis.connectrpc_es/google/bytestream_connect.js"; + import { ByteStream } from "@buf/googleapis_googleapis.bufbuild_es/google/bytestream_pb.js"; ``` ## Gotchas ### The new plugins generates missing imports Because Protobuf-ES supports custom options and other reflection-based features now, generated code includes more information than in the previous version, and will generate additional imports in some situations. For example, if you have a Protobuf message that uses validation rules from [buf.build/bufbuild/protovalidate](https://buf.build/bufbuild/protovalidate), the Protobuf file has an import for the validation options: ```protobuf import "buf/validate/validate.proto"; ``` The old plugin ignored this import, but the new plugin will generate a corresponding ECMAScript import: ```diff + import { file_buf_validate_validate } from "./buf/validate/validate_pb"; ``` The imported file is not generated by default. To include imports, add the following option to your buf.gen.yaml config: ```diff # buf.gen.yaml version: v2 plugins: - local: protoc-gen-es out: src/gen + include_imports: true ``` ### Parcel fails to resolve imports Connect-ES and Protobuf-ES use [package exports](https://nodejs.org/docs/latest-v12.x/api/packages.html#packages_exports). If you see the following error with Parcel, make sure to [enable package exports](https://parceljs.org/features/dependency-resolution/#package-exports): ``` @parcel/core: Failed to resolve '@bufbuild/protobuf/codegenv1' ``` ### Metro fails to resolve imports Connect-ES and Protobuf-ES use [package exports](https://nodejs.org/docs/latest-v12.x/api/packages.html#packages_exports). If you see the following error with Metro or Expo, make sure to [enable package exports](https://metrobundler.dev/docs/package-exports/): ``` Metro error: Unable to resolve module @bufbuild/protobuf/codegenv1 ``` ### Type ... is not assignable to type ... Previously, Connect allowed request objects with matching shapes to be passed to API calls interchangeably as long as the passed object was a superset of the target type. For example, given the following proto definitions: ```protobuf syntax = "proto3"; package example.v1; message MessageA { string field_a = 1; } message MessageB { string field_a = 1; int64 field_b = 2; } service ExampleService { rpc RequestA(MessageA) returns (Empty) {} rpc RequestB(MessageB) returns (Empty) {} } ``` The following would have passed TypeScript compilation: ```ts client.requestA(new MessageA()); client.requestA(new MessageB()); ``` This was an unintended bug and not a feature. In Connect v2, only the specified target type will pass compilation. ```ts client.requestA(create(MessageASchema)); client.requestA(create(MessageBSchema)); // Type Error: Argument of type MessageBSchema is not assignable to parameter of type MessageInit<MessageASchema> ``` If you intend to pass a message as a different message with the same fields, you can use object destructuring to drop the `$typeName`, and copy the rest of the properties: ```ts const messageA: MessageA = ...; const { $typeName: _, ...properties } = messageA; const messageB = create(MessageBSchema, properties); ```