TL;DR
Build multi-protocol APIs by separating business logic from protocol layers. Create a shared domain layer, then add REST, GraphQL, and gRPC adapters on top. Modern PetstoreAPI demonstrates this architecture with consistent data models across all three protocols.
Introduction
Your API serves web clients, mobile apps, and internal microservices. Web clients want REST for simplicity. Mobile apps want GraphQL to reduce data transfer. Microservices want gRPC for performance. Do you build three separate APIs?
No. You build one API with three protocol layers. The business logic stays the same. Only the protocol adapters change. This is multi-protocol API architecture.
Modern PetstoreAPI implements REST, GraphQL, and gRPC from a shared core. The same pet store logic serves all three protocols with consistent behavior.
In this guide, you’ll learn how to architect multi-protocol APIs, implement protocol layers, and ensure consistency across protocols using Modern PetstoreAPI as a reference.
Multi-Protocol Architecture
Multi-protocol APIs separate concerns into layers.
Layered Architecture
┌─────────────────────────────────────────┐
│ Protocol Layer (REST/GraphQL/gRPC) │
├─────────────────────────────────────────┤
│ Application Layer (Use Cases) │
├─────────────────────────────────────────┤
│ Domain Layer (Business Logic) │
├─────────────────────────────────────────┤
│ Data Layer (Database, Cache) │
└─────────────────────────────────────────┘
Protocol Layer: Handles HTTP, GraphQL queries, gRPC calls Application Layer: Orchestrates use cases (create pet, place order) Domain Layer: Business rules (validation, calculations) Data Layer: Persistence (database, cache)
Key Principles
1. Protocol-agnostic core
Business logic doesn’t know about HTTP, GraphQL, or gRPC. It works with domain objects.
2. Thin protocol adapters
Protocol layers translate between protocol formats and domain objects. They don’t contain business logic.
3. Shared data models
All protocols use the same domain models internally, ensuring consistency.
4. Independent deployment
Each protocol can be deployed separately if needed.
Shared Domain Layer
The domain layer contains business logic shared across all protocols.
Domain Models
// Domain model (protocol-agnostic)
class Pet {
id: string;
name: string;
species: Species;
status: PetStatus;
price: number;
constructor(data: PetData) {
this.validate(data);
Object.assign(this, data);
}
validate(data: PetData): void {
if (!data.name || data.name.length < 2) {
throw new ValidationError('Name must be at least 2 characters');
}
if (data.price < 0) {
throw new ValidationError('Price cannot be negative');
}
}
adopt(userId: string): Order {
if (this.status !== PetStatus.AVAILABLE) {
throw new BusinessError('Pet is not available for adoption');
}
this.status = PetStatus.ADOPTED;
return new Order({
petId: this.id,
userId,
total: this.price
});
}
}
This model works for REST, GraphQL, and gRPC. The validation and business rules are the same.
Use Cases
// Use case (protocol-agnostic)
class AdoptPetUseCase {
constructor(
private petRepository: PetRepository,
private orderRepository: OrderRepository
) {}
async execute(petId: string, userId: string): Promise<Order> {
const pet = await this.petRepository.findById(petId);
if (!pet) {
throw new NotFoundError('Pet not found');
}
const order = pet.adopt(userId);
await this.petRepository.save(pet);
await this.orderRepository.save(order);
return order;
}
}
This use case works regardless of whether it’s called from REST, GraphQL, or gRPC.
REST Protocol Layer
The REST layer translates HTTP requests to domain operations.
REST Controller
// REST adapter
class PetsController {
constructor(private adoptPetUseCase: AdoptPetUseCase) {}
async adoptPet(req: Request, res: Response): Promise<void> {
try {
const { petId } = req.params;
const { userId } = req.body;
const order = await this.adoptPetUseCase.execute(petId, userId);
res.status(201).json({
id: order.id,
petId: order.petId,
userId: order.userId,
total: order.total,
status: order.status
});
} catch (error) {
this.handleError(error, res);
}
}
private handleError(error: Error, res: Response): void {
if (error instanceof NotFoundError) {
res.status(404).json({
type: 'https://petstoreapi.com/errors/not-found',
title: 'Not Found',
status: 404,
detail: error.message
});
} else if (error instanceof ValidationError) {
res.status(400).json({
type: 'https://petstoreapi.com/errors/validation-error',
title: 'Validation Error',
status: 400,
detail: error.message
});
} else {
res.status(500).json({
type: 'https://petstoreapi.com/errors/internal-error',
title: 'Internal Server Error',
status: 500
});
}
}
}
REST Routes
app.post('/v1/pets/:petId/adopt', (req, res) =>
petsController.adoptPet(req, res)
);
Modern PetstoreAPI REST endpoints
GraphQL Protocol Layer
The GraphQL layer translates GraphQL queries to domain operations.
GraphQL Schema
type Pet {
id: ID!
name: String!
species: Species!
status: PetStatus!
price: Float!
}
type Order {
id: ID!
petId: ID!
userId: ID!
total: Float!
status: OrderStatus!
}
type Mutation {
adoptPet(petId: ID!, userId: ID!): Order!
}
GraphQL Resolver
// GraphQL adapter
const resolvers = {
Mutation: {
adoptPet: async (
_parent: any,
args: { petId: string; userId: string },
context: Context
): Promise<Order> => {
try {
return await context.adoptPetUseCase.execute(
args.petId,
args.userId
);
} catch (error) {
throw new GraphQLError(error.message, {
extensions: {
code: error instanceof NotFoundError ? 'NOT_FOUND' :
error instanceof ValidationError ? 'BAD_USER_INPUT' :
'INTERNAL_SERVER_ERROR'
}
});
}
}
}
};
GraphQL Request
mutation {
adoptPet(
petId: "019b4132-70aa-764f-b315-e2803d882a24"
userId: "user-123"
) {
id
total
status
}
}
Modern PetstoreAPI GraphQL schema
gRPC Protocol Layer
The gRPC layer translates Protocol Buffer messages to domain operations.
Protocol Buffer Definition
syntax = "proto3";
package petstore.v1;
service PetService {
rpc AdoptPet(AdoptPetRequest) returns (AdoptPetResponse);
}
message AdoptPetRequest {
string pet_id = 1;
string user_id = 2;
}
message AdoptPetResponse {
string order_id = 1;
string pet_id = 2;
string user_id = 3;
double total = 4;
string status = 5;
}
gRPC Service Implementation
// gRPC adapter
class PetServiceImpl implements IPetService {
constructor(private adoptPetUseCase: AdoptPetUseCase) {}
async adoptPet(
call: ServerUnaryCall<AdoptPetRequest, AdoptPetResponse>,
callback: sendUnaryData<AdoptPetResponse>
): Promise<void> {
try {
const { petId, userId } = call.request;
const order = await this.adoptPetUseCase.execute(petId, userId);
callback(null, {
orderId: order.id,
petId: order.petId,
userId: order.userId,
total: order.total,
status: order.status
});
} catch (error) {
callback({
code: error instanceof NotFoundError ? status.NOT_FOUND :
error instanceof ValidationError ? status.INVALID_ARGUMENT :
status.INTERNAL,
message: error.message
});
}
}
}
Modern PetstoreAPI gRPC service
How Modern PetstoreAPI Implements Multi-Protocol
Modern PetstoreAPI demonstrates multi-protocol architecture with real examples.
Architecture Overview
Modern PetstoreAPI
├── Domain Layer
│ ├── Pet (entity)
│ ├── Order (entity)
│ └── Use Cases
│ ├── CreatePet
│ ├── AdoptPet
│ └── PlaceOrder
├── REST Layer
│ ├── /v1/pets
│ ├── /v1/orders
│ └── OpenAPI 3.2 spec
├── GraphQL Layer
│ ├── Query resolvers
│ ├── Mutation resolvers
│ └── GraphQL schema
└── gRPC Layer
├── PetService
├── OrderService
└── .proto definitions
Consistent Data Models
All protocols return the same data:
REST:
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
GraphQL:
{
"data": {
"pet": {
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
}
}
gRPC:
{
pet_id: "019b4132-70aa-764f-b315-e2803d882a24"
name: "Fluffy"
species: CAT
}
Same data, different formats.
Shared Validation
Validation happens in the domain layer, so all protocols enforce the same rules:
// Domain validation (shared)
if (name.length < 2) {
throw new ValidationError('Name must be at least 2 characters');
}
REST returns:
{
"type": "https://petstoreapi.com/errors/validation-error",
"status": 400,
"detail": "Name must be at least 2 characters"
}
GraphQL returns:
{
"errors": [{
"message": "Name must be at least 2 characters",
"extensions": {"code": "BAD_USER_INPUT"}
}]
}
gRPC returns:
code: INVALID_ARGUMENT
message: "Name must be at least 2 characters"
Same validation, protocol-specific error formats.
Testing Multi-Protocol APIs with Apidog
Apidog supports testing all three protocols.
Test REST Endpoint
POST https://petstoreapi.com/v1/pets/019b4132-70aa-764f-b315-e2803d882a24/adopt
Content-Type: application/json
{
"userId": "user-123"
}
Test GraphQL Mutation
mutation {
adoptPet(
petId: "019b4132-70aa-764f-b315-e2803d882a24"
userId: "user-123"
) {
id
total
}
}
Test gRPC Service
grpc.petstore.v1.PetService/AdoptPet
{
"pet_id": "019b4132-70aa-764f-b315-e2803d882a24",
"user_id": "user-123"
}
Verify Consistency
Test that all three protocols return the same data:
- Call REST endpoint
- Call GraphQL mutation
- Call gRPC service
- Compare results
Apidog can automate this cross-protocol testing.
Deployment Strategies
Strategy 1: Single Service
Deploy all protocols in one service:
┌─────────────────────────┐
│ PetstoreAPI Service │
│ ├── REST (port 8080) │
│ ├── GraphQL (port 8081)│
│ └── gRPC (port 50051) │
└─────────────────────────┘
Pros: Simple deployment, shared resources Cons: All protocols scale together
Strategy 2: Separate Services
Deploy each protocol separately:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ REST Service │ │ GraphQL Svc │ │ gRPC Service │
│ (port 8080) │ │ (port 8081) │ │ (port 50051) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────┴──────────────────┘
│
┌──────────────┐
│ Shared Core │
│ Library │
└──────────────┘
Pros: Independent scaling, protocol isolation Cons: More complex deployment
Strategy 3: API Gateway
Use an API gateway to route to protocol-specific backends:
┌─────────────┐
│ API Gateway │
└─────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌───────▼──────┐ ┌────▼─────┐ ┌──────▼──────┐
│ REST Backend │ │ GraphQL │ │ gRPC Backend│
└──────────────┘ └──────────┘ └─────────────┘
Pros: Centralized routing, rate limiting, auth Cons: Additional latency, gateway complexity
Modern PetstoreAPI uses Strategy 1 for simplicity.
Conclusion
Multi-protocol APIs give clients flexibility without duplicating business logic. By separating protocol layers from domain logic, you can support REST, GraphQL, and gRPC from a shared core.
Modern PetstoreAPI demonstrates this architecture with consistent data models, shared validation, and protocol-specific adapters. Whether clients use REST for simplicity, GraphQL for flexibility, or gRPC for performance, they access the same pet store with the same business rules.
Test your multi-protocol APIs with Apidog to ensure consistency across protocols. Explore Modern PetstoreAPI to see multi-protocol architecture in action.
FAQ
Do I need to support all three protocols?
No. Start with REST for public APIs. Add GraphQL if clients need flexible data fetching. Add gRPC for internal microservices. Only add protocols when you have a clear use case.
How do I keep protocols consistent?
Share business logic in a domain layer. Protocol adapters should only translate formats, not contain business rules. Test all protocols to verify they return the same data.
Can I version protocols independently?
Yes. REST can be at v2 while GraphQL is at v1. But this creates complexity. Try to keep protocols in sync when possible.
How do I handle authentication across protocols?
Use the same authentication in all protocols. REST uses Bearer tokens in headers. GraphQL uses the same tokens. gRPC uses metadata. The domain layer validates tokens the same way.
What about WebSocket and SSE?
WebSocket and SSE are transport protocols, not API protocols. You can add them alongside REST/GraphQL/gRPC for real-time updates. Modern PetstoreAPI includes both.
How do I document multi-protocol APIs?
Use OpenAPI for REST, GraphQL schema for GraphQL, and .proto files for gRPC. Modern PetstoreAPI provides all three at https://docs.petstoreapi.com/
Can I use different databases for different protocols?
Yes, but it creates consistency challenges. Better to use the same data layer for all protocols. Let the protocol adapters handle format differences.
How do I test multi-protocol APIs?
Use Apidog to test all protocols in one tool. Create test suites that verify consistency across protocols. Test that REST, GraphQL, and gRPC return the same data for the same operations.
