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
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) etcallback(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