diff --git a/docs/guide/dubboForNode/GettingStarted.md b/docs/guide/dubboForNode/GettingStarted.md index aa0c9abd..202832c8 100644 --- a/docs/guide/dubboForNode/GettingStarted.md +++ b/docs/guide/dubboForNode/GettingStarted.md @@ -1 +1,211 @@ -# GettingStarted +# Getting started + +Dubbo-js is a library for serving Dubbo, gRPC, and gRPC-Web compatible HTTP APIs using Node.js. It brings the Dubbo +Protocol to Node with full TypeScript compatibility and support for all four types of remote procedure calls: unary and +the three variations of streaming. + +This ten-minute walkthrough helps you create a small Dubbo service in Node.js. It demonstrates what you'll be writing by +hand, what Connect generates for you, and how to call your new API. + +# Prerequisites + +We'll set up a project from scratch and then augment it to serve a new endpoint. + +- You'll need [Node.js](https://nodejs.org/en/download) installed - we recommend the most recent long-term support + version (LTS). +- We'll use the package manager `npm`, but we are also compatible with `yarn` and `pnpm`. +- We'll also use [cURL](https://curl.se/). It's available from Homebrew and most Linux package managers. + +# Project setup + +Let's initialize a project with TypeScript, and install some code generation tools: + +```shell +mkdir dubbo-example +cd dubbo-example +npm init -y +npm install typescript tsx +npx tsc --init +npm install @bufbuild/protoc-gen-es @bufbuild/protobuf @apachedubbo/protoc-gen-apache-dubbo-es @apachedubbo/dubbo +``` + +# Define a service + +First, we need to add a Protobuf file that includes our service definition. For this tutorial, we are going to construct +a unary endpoint for a service that is a stripped-down implementation of [ELIZA](https://en.wikipedia.org/wiki/ELIZA), +the famous natural language processing program. + +```shell +mkdir -p proto && touch proto/example.proto +``` + +Open up the above file and add the following service definition: + +```Protobuf +syntax = "proto3"; + +package apache.dubbo.demo.example.v1; + +message SayRequest { + string sentence = 1; +} + +message SayResponse { + string sentence = 1; +} + +service ExampleService { + rpc Say(SayRequest) returns (SayResponse) {} +} +``` + +# Generate code + +Create the gen directory as the target directory for generating file placement: + +```Shell +mkdir -p gen +``` + +Run the following command to generate a code file in the gen directory: + +```Shell +PATH=$PATH:$(pwd)/node_modules/.bin \ + protoc -I proto \ + --es_out gen \ + --es_opt target=ts \ + --apache-dubbo-es_out gen \ + --apache-dubbo-es_opt target=ts \ + example.proto +``` + +After running the command, the following generated files should be visible in the target directory: + +```Plain Text +├── gen +│ ├── example_dubbo.ts +│ └── example_pb.ts +├── proto +│ └── example.proto +``` + +Next, we are going to use these files to implement our service. + +# Implement the service + +We defined the `ElizaService` - now it's time to implement it, and register it with the `DubboRouter`. First, let's +create a file where we can put the implementation: + +Create a new file `dubbo.ts` with the following contents: + +```typescript +import type { DubboRouter } from "@apachedubbo/dubbo"; +import { ExampleService } from "./gen/example_dubbo"; + +export default (router: DubboRouter) => + // registers apache.dubbo.demo.example.v1 + router.service(ExampleService, { + // implements rpc Say + async say(req) { + return { + sentence: `You said: ${req.sentence}`, + }; + }, + }, { serviceGroup: 'dubbo', serviceVersion: '1.0.0' }); +``` + +That's it! There are many other alternatives to implementing a service, and you have access to a context object for +headers and trailers, but let's keep it simple for now. + +# Start a server + +Dubbo services can be plugged into vanilla Node.js +servers, [Next.js](https://nextjs.org/), [Express](https://expressjs.com/), or [Fastify](https://fastify.dev/). We are +going to use Fastify here. Let's install it, along with our plugin for Fastify: + +```shell +npm install fastify @apachedubbo/dubbo-fastify +``` + +Create a new file `server.ts` with the following contents and register the `ExampleService` implemented in the previous +step with it. +Next, you can directly initialize and start the server, which will receive requests on the specified port: + +```typescript +import { fastify } from "fastify"; +import { fastifyDubboPlugin } from "@apachedubbo/dubbo-fastify"; +import routes from "./dubbo"; + +async function main() { + const server = fastify(); + await server.register(fastifyDubboPlugin, { + routes, + }); + server.get("/", (_, reply) => { + reply.type("text/plain"); + reply.send("Hello World!"); + }); + await server.listen({ host: "localhost", port: 8080 }); + console.log("server is listening at", server.addresses()); +} + +void main(); +``` + +Congratulations. Your endpoint is ready to go! You can start your server with: + +```Shell +npx tsx server.ts +``` + +# Make requests + +The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of cURL +installed, it's a one-liner: + +```Shell +curl \ + --header 'Content-Type: application/json' \ + --header 'TRI-Service-Version: 1.0.0' \ + --header 'TRI-Service-group: dubbo' \ + --data '{"sentence": "Hello World"}' \ + http://localhost:8080/apache.dubbo.demo.example.v1.ExampleService/Say +``` + +You can also use the standard Dubbo client request service. First, we need to obtain the service proxy from the +generated code, which is the Dubbo node package, specify the server address for it, and initialize it. Then, we can +initiate an RPC call. + +Create a `client.ts` file. + +```typescript +import { createPromiseClient } from "@apachedubbo/dubbo"; +import { ExampleService } from "./gen/example_dubbo"; +import { createDubboTransport } from "@apachedubbo/dubbo-node"; + +const transport = createDubboTransport({ + baseUrl: "http://localhost:8080", + httpVersion: "1.1", +}); + +async function main() { + const client = createPromiseClient(ExampleService, transport, { serviceVersion: '1.0.0', serviceGroup: 'dubbo' }); + const res = await client.say({ sentence: "Hello World" }); + console.log(res); +} + +void main(); +``` + +Run client: + +```Shell +npx tsx client.ts +``` + +--- + +# Others + +Refer to [Developing Web Applications Running on Browsers](../dubboForWEB/GettingStarted.md) to learn how to develop +browser pages that can access Dubbo backend services. diff --git a/docs/guide/dubboForNode/ImplementingServices.md b/docs/guide/dubboForNode/ImplementingServices.md index 20bf1086..5a4c7170 100644 --- a/docs/guide/dubboForNode/ImplementingServices.md +++ b/docs/guide/dubboForNode/ImplementingServices.md @@ -1 +1,248 @@ # ImplementingServices + +Dubbo handles HTTP routes and most plumbing for you, but implementing the actual business logic is still up to you. + +You always register your implementation on the `DubboRouter`. We recommend to create a file `dubbo.ts` with a registration +function in your project: + +```ts +import type { DubboRouter } from "@apachedubbo/dubbo"; + +export default (router: DubboRouter) => { +} +``` + +# Register a service + +Let's say you have defined a simple service in Protobuf: + +``` +message SayRequest { + string sentence = 1; +} +message SayResponse { + string sentence = 1; +} +service ElizaService { + rpc Say(SayRequest) returns (SayResponse) {} +} +``` + +To register this service, call `router.service()`: + +```ts +import { DubboRouter, HandlerContext } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { SayRequest, SayResponse } from "./gen/eliza_pb"; + +export default (router: DubboRouter) => + router.service(ElizaService, { + async say(req: SayRequest, context: HandlerContext) { + return new SayResponse({ + sentence: `You said ${req.sentence}`, + }); + } + }); +``` + +Your method `say()` receives the request message and a context object, and returns a response message. It is a plain +function! + +# Plain functions + +Your function can return a response message, or a promise for a response message, or just an initializer for a response +message: + +```ts +function say(req: SayRequest) { + return new SayResponse({ sentence: `You said ${req.sentence}` }); +} +``` + +```ts +async function say(req: SayRequest) { + return { sentence: `You said ${req.sentence}` }; +} +``` + +```ts +const say = (req: SayRequest) => ({ sentence: `You said ${req.sentence}` }); +``` + +You can register any of these functions for the ElizaService. + +# Context + +The context argument gives you access to headers and service metadata: + +```ts +import { HandlerContext } from "@apachedubbo/dubbo"; +import { SayRequest } from "./gen/eliza_pb"; + +function say(req: SayRequest, context: HandlerContext) { + ctx.service.typeName; // the protobuf type name "ElizaService" + ctx.method.name; // the protobuf rpc name "Say" + context.requestHeader.get("Foo"); + context.responseHeader.set("Foo", "Bar"); + return new SayResponse({ sentence: `You said ${req.sentence}` }); +} +``` + +It can also be used to access arbitrary values that are passed from either server plugins or interceptors. Please refer +to the docs on [interceptors](Interceptors.md) for learn more. + +# Errors + +Instead of returning a response, your method can also raise an error: + +```ts +import { Code, DubboError } from "@apachedubbo/dubbo"; + +function say() { + throw new DubboError("I have no words anymore.", Code.ResourceExhausted); +} +``` + +`Code` is one of Connects [error codes](). Besides the code and a message, errors can also contain metadata (a Headers +object) and error details. + +# Error details + +Error details are a powerful feature. Any protobuf message can be transmitted as an error detail. Let's +use `google.rpc.LocalizedMessage` to localize our error message: + +```shell +buf generate buf.build/googleapis/googleapis +``` + +```ts +import { Code, DubboError } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { LocalizedMessage } from "./gen/google/rpc/error_details_pb"; + +function say() { + const details = [ + new LocalizedMessage({ + locale: "fr-CH", + message: "Je n'ai plus de mots.", + }), + new LocalizedMessage({ + locale: "ja-JP", + message: "もう言葉がありません。", + }), + ]; + const metadata = new Headers({ + "words-left": "none" + }); + throw new DubboError( + "I have no words anymore.", + Code.ResourceExhausted, + metadata, + details + ); +} +``` + +# Streaming + +Before showing the various handlers for streaming endpoints, we'd like to reference the [Streaming]() page from Dubbo-Go +as a caveat. Because while Dubbo for Node.js does support all three variations of streaming endpoints, there are +tradeoffs that should be considered before diving in. + +Streaming can be a very powerful approach to APIs in the right circumstances, but it also requires great care. Remember, +with great power comes great responsibility. + +In **client streaming**, the client sends multiple messages. Once the server receives all the messages, it responds with +a single message. In Protobuf schemas, client streaming methods look like this: + +``` +service ElizaService { + rpc Vent(stream VentRequest) returns (VentResponse) {} +} +``` + +In TypeScript, client streaming methods receive an asynchronous iterable of request messages (you can iterate over them +with a for [await...of](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) +loop): + +```ts +async function vent(reqs: AsyncIterable): Promise { +} +``` + +In **server streaming**, the client sends a single message, and the server responds with multiple messages. In Protobuf +schemas, server streaming methods look like this: + +``` +service ElizaService { + rpc Introduce(IntroduceRequest) returns (stream IntroduceResponse) {} +} +``` + +In TypeScript, server streaming methods receive a request message, and return an asynchronous iterable of response +messages, typically with +a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). + +```ts +async function* introduce(req: IntroduceRequest) { + yield { sentence: `Hi ${req.name}, I'm eliza` }; + yield { sentence: `How are you feeling today?` }; +} +``` + +In **bidirectional streaming** (often called bidi), the client and server may both send multiple messages. Often, the +exchange is structured like a conversation: the client sends a message, the server responds, the client sends another +message, and so on. Keep in mind that this always requires end-to-end HTTP/2 support (regardless of RPC protocol)! + +# Helper Types + +Service implementations are type-safe. The `service()` method of the `DubboRouter` accepts a `ServiceImpl`, where `T` +is a service type. A `ServiceImpl` has a method for each RPC, typed as `MethodImp`, where `M` is a method info +object. + +You can use these types to compose your service without registering it right away: + +```ts +import type { MethodImpl, ServiceImpl } from "@apachedubbo/dubbo"; + +export const say: MethodImpl = +... + +export const eliza: ServiceImpl = { + // ... +}; + +export class Eliza implements ServiceImpl { + async say(req: SayRequest) { + return { + sentence: `You said ${req.sentence}`, + }; + } +} +``` + +Registering the examples above: + +```ts +import type { DubboRouter } from "@apachedubbo/dubbo"; +import { ElizaService } from "./gen/eliza_dubbo"; +import { say, eliza, Eliza } from "./other-file"; + +export default (router: DubboRouter) => { + // using const say + router.service(ElizaService, { say }); + + // alternative for using const say + router.rpc( + ElizaService, + ElizaService.methods.say, + say + ); + + // using const eliza + router.service(ElizaService, eliza); + + // using class Eliza + router.service(ElizaService, new Eliza()); +} +```