REST & gRPC — Practical
REST & gRPC — Practical patterns
Section titled “REST & gRPC — Practical patterns”REST: standard problem-detail error response
Section titled “REST: standard problem-detail error response”{ "type": "https://api.example.com/errors/invalid-email", "title": "Invalid email", "status": 422, "detail": "Email 'foo@' is not valid", "instance": "/users", "fieldErrors": [{ "field": "email", "code": "format" }]}REST: cursor pagination response
Section titled “REST: cursor pagination response”{ "data": [ { ... }, { ... } ], "page": { "next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDUtMDFUMTI6MDA6MDBaIn0=", "has_more": true }}// Express handlerapp.get('/posts', async (req, res) => { const limit = Math.min(Number(req.query.limit ?? 20), 100); const cursor = req.query.cursor ? decodeCursor(req.query.cursor) : null; const rows = await db.posts.list({ limit: limit + 1, cursor }); const hasMore = rows.length > limit; if (hasMore) rows.pop(); res.json({ data: rows, page: { next_cursor: hasMore ? encodeCursor(rows.at(-1)!) : null, has_more: hasMore }, });});REST: ETag conditional GET
Section titled “REST: ETag conditional GET”app.get('/users/:id', async (req, res) => { const u = await db.users.find(req.params.id); const etag = `"${hash(u.updatedAt + u.id)}"`; if (req.headers['if-none-match'] === etag) return res.status(304).end(); res.set('ETag', etag).json(u);});gRPC: server (Node, @grpc/grpc-js)
Section titled “gRPC: server (Node, @grpc/grpc-js)”import * as grpc from '@grpc/grpc-js';import { UsersService } from './gen/users';
const server = new grpc.Server();server.addService(UsersService, { async get(call, cb) { try { const u = await db.users.find(call.request.id); if (!u) return cb({ code: grpc.status.NOT_FOUND, message: 'user' }); cb(null, u); } catch (e: any) { cb({ code: grpc.status.INTERNAL, message: e.message }); } }, list(call) { db.users.stream().on('data', u => call.write(u)).on('end', () => call.end()); },});server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => server.start());gRPC: client with deadline
Section titled “gRPC: client with deadline”const client = new UsersClient('users:50051', grpc.credentials.createInsecure());const deadline = new Date(Date.now() + 1500);client.get({ id: '123' }, { deadline }, (err, user) => { ... });gRPC: client streaming upload
Section titled “gRPC: client streaming upload”const call = client.upload((err, summary) => console.log(summary));for (const chunk of chunks) call.write({ data: chunk });call.end();gRPC: bidirectional stream
Section titled “gRPC: bidirectional stream”const call = client.chat();call.on('data', msg => console.log('recv', msg));call.write({ text: 'hi' });process.stdin.on('data', d => call.write({ text: d.toString() }));OpenAPI 3 spec sketch
Section titled “OpenAPI 3 spec sketch”openapi: 3.0.3info: { title: Users, version: '1.0' }paths: /users/{id}: get: parameters: - in: path name: id required: true schema: { type: string } responses: '200': { description: ok, content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } } '404': { description: not found }components: schemas: User: type: object required: [id, email] properties: id: { type: string } email: { type: string, format: email } createdAt: { type: string, format: date-time }Idempotency-Key middleware
Section titled “Idempotency-Key middleware”const idem = async (req, res, next) => { const key = req.header('Idempotency-Key'); if (!key) return res.status(400).json({ error: 'missing idempotency-key' }); const cached = await redis.get(`idem:${req.path}:${key}`); if (cached) { res.set('Idempotent-Replay', '1'); return res.json(JSON.parse(cached)); } res.locals.idemKey = key; next();};
app.post('/payments', idem, async (req, res) => { const result = await processPayment(req.body); await redis.set(`idem:${req.path}:${res.locals.idemKey}`, JSON.stringify(result), 'EX', 86400); res.json(result);});Versioning sketch
Section titled “Versioning sketch”/v1/users (frozen)/v2/users (current)Or content-negotiated:
GET /usersAccept: application/vnd.acme.v2+jsonUseful tools
Section titled “Useful tools”- buf — protobuf linter, breaking-change detector, codegen.
- grpcurl —
grpcurl -plaintext localhost:50051 list. - ghz — gRPC load test.
- k6 / wrk / autocannon — REST load test.
- httpie — friendlier curl.
- OpenAPI generator — generate clients/servers in many langs.
- Postman / Insomnia — both support REST and gRPC.
- Envoy — gRPC-Web bridge, retries, timeouts, metrics.
Common gotchas
Section titled “Common gotchas”- Forgetting deadline on gRPC call → caller hangs forever.
- gRPC error from interceptor doesn’t include trailers if framing wrong.
- Returning protobuf default values means “field not set” — use
optionalfor nullable scalars. - REST API growing optional query params indefinitely — use POST with body for complex search.
- Logging bearer tokens — strip auth headers before logging.