gRPC - 02 - Creer un serveur gRPC en TypeScript

Implementer un serveur gRPC en TypeScript avec @grpc/grpc-js. Du fichier .proto au serveur fonctionnel, étape par étape.

02 - Creer un serveur gRPC en TypeScript

Ce que tu vas apprendre

  • Mettre en place un serveur gRPC complet en TypeScript
  • Implementer les handlers de service
  • Gerer les erreurs avec les status codes gRPC
  • Tester ton serveur avec grpcurl

Prerequisites

01 - Protocol Buffers


Le setup

On va construire un serveur gRPC pour un service de gestion de taches. Simple, concret, et suffisant pour montrer tous les concepts.

D'abord, le fichier .proto :

protobuf// proto/task.proto
syntax = "proto3";

package taskmanager;

service TaskService {
  rpc CreateTask (CreateTaskRequest) returns (Task);
  rpc GetTask (GetTaskRequest) returns (Task);
  rpc ListTasks (ListTasksRequest) returns (ListTasksResponse);
  rpc UpdateTask (UpdateTaskRequest) returns (Task);
  rpc DeleteTask (DeleteTaskRequest) returns (DeleteTaskResponse);
}

message Task {
  string id = 1;
  string title = 2;
  string description = 3;
  TaskStatus status = 4;
  int64 created_at = 5;
  int64 updated_at = 6;
}

enum TaskStatus {
  TASK_STATUS_UNSPECIFIED = 0;
  TASK_STATUS_TODO = 1;
  TASK_STATUS_IN_PROGRESS = 2;
  TASK_STATUS_DONE = 3;
}

message CreateTaskRequest {
  string title = 1;
  string description = 2;
}

message GetTaskRequest {
  string task_id = 1;
}

message ListTasksRequest {
  TaskStatus status_filter = 1;
  int32 page_size = 2;
}

message ListTasksResponse {
  repeated Task tasks = 1;
}

message UpdateTaskRequest {
  string task_id = 1;
  string title = 2;
  string description = 3;
  TaskStatus status = 4;
}

message DeleteTaskRequest {
  string task_id = 1;
}

message DeleteTaskResponse {
  bool success = 1;
}

Installation des dépendances

bashnpm init -y
npm install @grpc/grpc-js @grpc/proto-loader
npm install -D typescript ts-proto @types/node

Genere le code TypeScript à partir du .proto :

bashnpx protoc \
  --plugin=./node_modules/.bin/protoc-gen-ts_proto \
  --ts_proto_out=./src/generated \
  --ts_proto_opt=outputServices=grpc-js \
  --ts_proto_opt=env=node \
  --ts_proto_opt=esModuleInterop=true \
  -I./proto \
  ./proto/task.proto

Ca généré un fichier src/generated/task.ts avec toutes les interfaces TypeScript et les définitions de service. Tu as maintenant le type Task, CreateTaskRequest, et surtout TaskServiceServer -- l'interface que ton serveur doit implementer.

L'implementation du serveur

typescript// src/server.ts
import * as grpc from "@grpc/grpc-js";
import { TaskServiceServer, TaskServiceService } from "./generated/task";
import { randomUUID } from "crypto";

// Store en memoire (en vrai, ce serait une base de donnees)
const tasks = new Map<string, {
  id: string;
  title: string;
  description: string;
  status: number;
  createdAt: number;
  updatedAt: number;
}>();

const taskService: TaskServiceServer = {
  createTask(call, callback) {
    const { title, description } = call.request;

    if (!title || title.trim() === "") {
      return callback({
        code: grpc.status.INVALID_ARGUMENT,
        message: "title is required"
      });
    }

    const now = Date.now();
    const task = {
      id: randomUUID(),
      title,
      description: description || "",
      status: 1, // TASK_STATUS_TODO
      createdAt: now,
      updatedAt: now
    };

    tasks.set(task.id, task);
    callback(null, task);
  },

  getTask(call, callback) {
    const { taskId } = call.request;
    const task = tasks.get(taskId);

    if (!task) {
      return callback({
        code: grpc.status.NOT_FOUND,
        message: `task ${taskId} not found`
      });
    }

    callback(null, task);
  },

  listTasks(call, callback) {
    const { statusFilter, pageSize } = call.request;
    let result = Array.from(tasks.values());

    if (statusFilter && statusFilter !== 0) {
      result = result.filter(t => t.status === statusFilter);
    }

    const limit = pageSize > 0 ? pageSize : 50;
    result = result.slice(0, limit);

    callback(null, { tasks: result });
  },

  updateTask(call, callback) {
    const { taskId, title, description, status } = call.request;
    const task = tasks.get(taskId);

    if (!task) {
      return callback({
        code: grpc.status.NOT_FOUND,
        message: `task ${taskId} not found`
      });
    }

    if (title) task.title = title;
    if (description) task.description = description;
    if (status && status !== 0) task.status = status;
    task.updatedAt = Date.now();

    tasks.set(taskId, task);
    callback(null, task);
  },

  deleteTask(call, callback) {
    const { taskId } = call.request;
    const existed = tasks.delete(taskId);
    callback(null, { success: existed });
  }
};

function main() {
  const server = new grpc.Server();
  server.addService(TaskServiceService, taskService);

  const address = "0.0.0.0:50051";
  server.bindAsync(address, grpc.ServerCredentials.createInsecure(), (err) => {
    if (err) {
      console.error("Failed to bind:", err);
      process.exit(1);
    }
    console.log(`gRPC server running on ${address}`);
  });
}

main();

C'est tout. Un serveur gRPC fonctionnel en ~80 lignes. Chaque handler recoit un call (la requête avec metadata) et un callback (pour envoyer la réponse ou l'erreur).

Les status codes gRPC

gRPC a ses propres codes d'erreur. Pas des 404 ou 500 comme en HTTP. Voici les plus utilises :

Code Nom Équivalent HTTP Usage
0 OK 200 Succes
3 INVALID_ARGUMENT 400 Requete invalide
5 NOT_FOUND 404 Ressource inexistante
7 PERMISSION_DENIED 403 Pas les droits
13 INTERNAL 500 Erreur serveur
14 UNAVAILABLE 503 Service indisponible
16 UNAUTHENTICATED 401 Non authentifié

Utilise le bon code. Un INTERNAL pour une requête invalide, c'est comme retourner un 500 quand l'utilisateur oublie un champ. Ca rend le debug penible.

typescript// Bonne gestion d'erreur
if (!taskId) {
  return callback({
    code: grpc.status.INVALID_ARGUMENT,
    message: "task_id is required"
  });
}

// Avec des details (metadata)
const metadata = new grpc.Metadata();
metadata.set("field", "task_id");
metadata.set("constraint", "required");
return callback({
  code: grpc.status.INVALID_ARGUMENT,
  message: "task_id is required",
  metadata
});

Metadata : les headers de gRPC

La metadata, c'est l'équivalent des headers HTTP. Tu peux y mettre des tokens d'authentification, des request IDs pour le tracing, ou des informations de contexte.

typescript// Lire la metadata cote serveur
getTask(call, callback) {
  const authToken = call.metadata.get("authorization")[0];
  const requestId = call.metadata.get("x-request-id")[0];

  if (!authToken) {
    return callback({
      code: grpc.status.UNAUTHENTICATED,
      message: "missing authorization token"
    });
  }

  // ... logique metier
}

Sur paltemps.fr, l'authentification passe par des headers HTTP classiques. En gRPC, c'est la metadata qui joue ce rôle. Le concept est le meme, le mecanisme change.

Tester avec grpcurl

grpcurl est l'équivalent de curl pour gRPC. Indispensable pour tester ton serveur.

bash# Installer grpcurl
brew install grpcurl  # macOS
# ou telecharger depuis github.com/fullstorydev/grpcurl

# Lister les services (necessite la reflection, voir plus bas)
grpcurl -plaintext localhost:50051 list

# Appeler CreateTask
grpcurl -plaintext -d '{"title": "Ecrire un article gRPC", "description": "Serie complete"}' \
  localhost:50051 taskmanager.TaskService/CreateTask

# Appeler GetTask
grpcurl -plaintext -d '{"taskId": "le-uuid-retourne"}' \
  localhost:50051 taskmanager.TaskService/GetTask

Pour que grpcurl list fonctionne, active la reflection sur ton serveur :

typescriptimport { addReflection } from "grpc-server-reflection";

// Apres server.addService(...)
addReflection(server, "proto/task.proto");

La reflection permet aux outils de découvrir les services et les messages sans avoir le fichier .proto. Utile pour le debug, indispensable en dev. L'article sur la production explique si tu dois la laisser active en prod ou non.

Structure du projet

task-service/
├── proto/
│   └── task.proto            # Le contrat
├── src/
│   ├── generated/
│   │   └── task.ts           # Code genere par ts-proto
│   └── server.ts             # Implementation du serveur
├── package.json
└── tsconfig.json

C'est une structure simple pour un service. Si tu veux aller plus loin dans l'organisation, la serie sur l'architecture hexagonale montre comment séparer la logique métier de l'infrastructure (y compris gRPC, qui serait un adaptateur driving).


Résumé

  • Un serveur gRPC en TypeScript = @grpc/grpc-js + un fichier .proto + des handlers
  • Les handlers recoivent call (requête + metadata) et callback (réponse ou erreur)
  • Utilise les bons status codes gRPC (pas INTERNAL pour tout)
  • La metadata est l'équivalent des headers HTTP
  • grpcurl + reflection pour tester ton serveur en dev

Article précédent : 01 - Protocol Buffers Article suivant : 03 - Le client gRPC en TypeScript

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.