
π Introduction
APIs are the backbone of modern distributed systems. Over time, API design evolved from ad-hoc HTTP endpoints to strongly typed contracts with dedicated tooling.
Example (Go, the βrawβ approach):
|
|
ποΈ API-First Approach
Before development even starts, the business and product teams should model the contract β not just developers.
This is called the API-First approach:
Define your OpenAPI or Protobuf contract up front.
Use it as the source of truth across teams (backend, frontend, QA).
Generate server stubs, clients, and mocks directly from the contract.
π Why it matters:
Aligns business expectations with implementation.
Reduces misunderstandings and integration surprises.
Enables parallel development (frontend builds against mocks while backend is still in progress).
π OpenAPI & Swagger
OpenAPI (OAS): machine-readable spec for REST APIs.
Swagger: ecosystem of tooling (UI, codegen, validators).
β Benefits:
Contracts as a single source of truth.
Auto-generate docs, clients, and server stubs.
Example OpenAPI snippet (YAML):
|
|
π Swagger UI: renders the spec into interactive docs.
π Try it online: Swagger Editor β design and validate REST APIs in OpenAPI format.
π¦ Protobuf Contracts
Protobuf defines schemas for gRPC services.
Provides strong typing, binary serialization, and evolution with backward compatibility.
Example .proto:
|
|
π Try it online: Buf Schema Registry β design, lint, and validate gRPC/Protobuf APIs in the browser.
π REST
Pros: simple, widely supported, cacheable.
Cons: over-fetching/under-fetching, no strong typing.
Example (Gin):
|
|
π GraphQL
Pros: client chooses fields, flexible queries.
Cons: complex server, N+1 query problem, caching harder.
Example query:
|
|
π WebSocket
Pros: bidirectional, low-latency, real-time.
Cons: harder to scale, stateful connections.
Example (Go):
|
|
β‘ gRPC
Pros: fast, strongly typed, streaming support, efficient over HTTP/2.
Cons: tooling overhead, browser requires gRPC-Web.
Example Go client:
|
|
π Visual Comparison
flowchart TB
REST["REST\n+ Simple\n+ Cacheable\n- Over/Under fetching"]
GraphQL["GraphQL\n+ Flexible queries\n- Complex server"]
WebSocket["WebSocket\n+ Real-time\n- Scaling issues"]
gRPC["gRPC\n+ Fast, typed\n+ Streaming\n- Tooling overhead"]
REST --- GraphQL --- WebSocket --- gRPC
π§ͺ Testing Your APIs
- Robust APIs deserve robust tests. Here are practical workflows:
π§° Toolbox
Contracts: OpenAPI/Swagger (REST), Protobuf (gRPC)
Manual: IntelliJ HTTP Client, IDEA gRPC Client
CLI: curl, grpcurl
GUI: Postman (supports REST, GraphQL, WebSocket, gRPC)
Fail-Fast Pattern
When testing, adopt a fail-fast mindset:
Detect invalid inputs and misconfigurations as early as possible.
Fail the request with clear error messages (e.g., 422 Unprocessable Entity) instead of letting bad data flow deeper.
In CI, stop the pipeline immediately when contract tests or smoke tests fail.
This pattern prevents small mistakes (like missing fields, wrong types, expired tokens) from becoming production outages.
Mocking External Dependencies
Your service rarely lives in isolation. It depends on:
Databases
External APIs (e.g., payment gateways, identity providers)
Message queues / event buses
For reliability, these dependencies should be:
Mocked: fake servers responding to API requests (e.g., Prism for REST, fake gRPC server).
Stubbed: minimal hardcoded responses (fast for unit tests).
Real: integration environment with actual dependencies (used sparingly).
π Testing pyramid for APIs:
Unit tests β use stubs.
Integration tests β use mocks.
E2E tests β hit real dependencies (but keep scope small).
Dependency Graph
flowchart LR
subgraph Service["Your API Service"]
A["Controller / Handler"]
B["Application Logic"]
end
subgraph Dependencies
DB[("Database")]
EXT1["External API\n(Payment Service)"]
EXT2["External API\n(Identity Provider)"]
MQ["Message Queue\n(Kafka/NATS)"]
end
subgraph TestDoubles["Test Doubles"]
M["Mocks\n(simulated servers)"]
S["Stubs\n(fixed responses)"]
end
A --> B
B --> DB
B --> EXT1
B --> EXT2
B --> MQ
%% Test mapping
B -.-> M
B -.-> S
β Use dependency injection in Go (pass interfaces, not concrete clients) β swap between real, mock, and stub implementations easily.
Example (Go interface for payment gateway):
|
|
During tests:
|
|
π Dependency Inversion Principle (DIP) in Practice
DIP:
High-level modules (use cases, services) must not depend on low-level modules (DB clients, HTTP SDKs).
Both should depend on abstractions (interfaces).
Abstractions shouldnβt depend on details; details depend on abstractions.
In Go, that means you define interfaces close to the domain/use case (core), and make adapters (DB, external APIs) implement them. Your wiring (composition root) injects the concrete implementations.
DIP Diagram (who depends on whom)
flowchart LR
subgraph Core["Core (Domain + Application)"]
S["Service / Use Case"]
P["Port Interface\n(e.g., PaymentGateway, UserRepo)"]
end
subgraph Adapters["Adapters (Infrastructure)"]
A1["PostgresUserRepo\nimplements UserRepo"]
A2["StripePayment\nimplements PaymentGateway"]
end
subgraph Drivers["Drivers (Transport)"]
H["HTTP/gRPC Handlers"]
end
%% Dependencies point TOWARD abstractions
H --> S
S --> P
A1 --> P
A2 --> P
Handlers depend on services (core).
Services depend on ports (interfaces).
Adapters implement ports and therefore depend on the interfaces (not the other way around).
Minimal Go example (Ports in core, Adapters implement)
File internal/core/payment/port.go
(β abstraction lives in the core)
|
|
File: internal/core/checkout/service.go
(high-level depends on abstraction)
|
|
File: internal/adapters/stripe/gateway.go
(detail depends on abstraction)
|
|
File: cmd/api/main.go
(composition root: wire details into core)
|
|
Testing with mocks/stubs (DIP makes this trivial)
File: internal/core/payment/stub.go
|
|
File: internal/core/checkout/service_test.go
|
|
DIP Checklist
Define ports (interfaces) in core (domain/application), not in infra.
Adapters implement ports; they import core, not vice versa.
Keep the composition root (wiring) at the edge (e.g., cmd/api/main.go).
In tests, replace adapters with stubs/mocks by injecting port implementations.
Enforce with var _ Port = (*Adapter)(nil) compile-time checks.
Avoid handlers or services importing vendor SDKsβthatβs a DIP smell.
This approach aligns perfectly with API-First and your mock/stub strategy: the contract (OpenAPI/Protobuf) defines shapes at the boundary, while DIP ensures your core stays independent from transport and vendor details.
π§ͺ When to Use Mock vs Stub
Use stubs for deterministic data, mocks for interaction/behavior verification.
Test level | Primary goal | Prefer Stub when⦠| Prefer Mock when⦠|
---|---|---|---|
Unit | Pure logic correctness | You just need canned data (happy/unhappy cases) | You must assert a call happened (args, order, count) |
Integration | Adapter correctness (DB/API/Queue) | You want fast, local tests without spinning dependencies | You need to simulate network errors, timeouts, retries |
E2E / Contract | End-to-end flows & schema compatibility | β | Use mock servers from OpenAPI/Protobuf for contract fit |
Stub (deterministic data for unit tests):
|
|
Mock (verify interaction):
|
|
Tips
Start with stubs for 80% of unit tests (fast, simple).
Use mocks where behavior matters (retry, circuit breaker, idempotency, audit).
In integration tests, you can run a mock server (Prism for REST, test gRPC server) to simulate realistic failures: 429/503, timeouts, malformed payloads.
π§ Code to an Interface, Not an Implementation
- Keep your core independent from frameworks and vendors. Depend on ports (interfaces) you own; inject adapters (details) at the edge.
Principles
Define interfaces in the core (domain/application).
Adapters implement those interfaces and import the core (not vice versa).
Wire everything in a composition root (cmd/*/main.go).
Prefer narrow, behavior-based interfaces (only what the use case needs).
Enforce compile-time checks: var _ Port = (*Adapter)(nil).
Bad (leaky) β core depends on vendor:
|
|
Good (DIP + interface) β core depends on abstraction:
|
|
Adapter implements the port:
|
|
Composition root wires details:
|
|
Interface design checklist
β Define in core (where itβs used), not in infra (where itβs implemented).
β Keep it minimal (YAGNI): expose only methods your use case truly needs.
β Return domain errors or wrap vendor errors at the boundary.
β Make it mock/stub-friendly (simple method signatures, context first arg).
β Avoid leaking transport types (no *http.Request or *sql.DB in ports).
IntelliJ IDEA HTTP Client
Create file
api_tests.http
:1 2 3 4 5 6 7 8 9 10 11 12 13
### Get user GET http://localhost:8080/v1/users/123 Authorization: Bearer {{TOKEN}} ### Create user POST http://localhost:8080/v1/users Content-Type: application/json Authorization: Bearer {{TOKEN}} { "name": "Alice", "email": "alice@example.com" }
GraphQL request:
1 2 3 4 5 6 7
POST http://localhost:8080/graphql Content-Type: application/json { "query": "query($id: ID!) { user(id: $id) { id name posts { title } } }", "variables": { "id": "123" } }
WebSocket request:
1 2 3 4 5
WEBSOCKET ws://localhost:8080/ws Sec-WebSocket-Protocol: chat < { "type": "hello", "payload": "Hi" } < { "type": "subscribe", "channel": "notifications" }
gRPC request:
1 2 3 4 5 6
GRPC localhost:50051 com.example.user.UserService/GetUser Content-Type: application/json { "id": "123" }
curl
&grpcurl
REST:
1 2
curl -sS -H "Authorization: Bearer $TOKEN" \ http://localhost:8080/v1/users/123 | jq
GraphQL:
1 2 3
curl -sS -H "Content-Type: application/json" \ -d '{"query":"{ user(id:\"123\"){ id name } }"}' \ http://localhost:8080/graphql | jq
gRPC:
1 2
grpcurl -plaintext -d '{"id":"123"}' \ localhost:50051 com.example.user.UserService.GetUser
Postman
Import OpenAPI β REST collections.
Import .proto β gRPC methods.
Use GraphQL tab for queries.
Use WebSocket request for real-time testing.
Contract-First Workflows
OpenAPI (REST): generate Go stubs with oapi-codegen.
Protobuf (gRPC): generate Go code with protoc.
Use mock servers (Prism, grpcurl) to test against the contract.
Add contract tests in CI to catch drift early.
Testing Patterns
Environments: manage tokens/URLs in IDEA http-client.env.json or Postman environments.
Negative tests: invalid payloads, auth errors, rate limiting.
Automation: run .http files or Postman collections in CI pipelines.
Quick Decision Matrix
Style Manual Tool CLI Contract Best For REST IDEA HTTP, Postman curl OpenAPI/Swagger β CRUD, β public APIs, β caching GraphQL IDEA HTTP, Postman curl SDL (schema) β Complex clients, β flexible reads, β οΈ caching harder WebSocket IDEA HTTP, Postman websocat (none) β Real-time, β chat, β dashboards, β οΈ scaling challenges gRPC IDEA gRPC, Postman grpcurl Protobuf β Service-to-service, β high perf, β streaming, β οΈ tooling overhead
π― When to Use Which?
REST β CRUD, public APIs, cache-friendly.
GraphQL β client-driven queries, mobile apps.
WebSocket β chat, dashboards, real-time collab.
gRPC β service-to-service, high-performance internal APIs.
Conclusion
First APIs were manual and error-prone.
OpenAPI/Swagger made REST self-documented.
Protobuf powers gRPC with strong contracts.
No silver bullet β choose API style based on domain, scale, and performance needs.
π Follow me on norbix.dev for more insights on Go, Python, AI, system design, and engineering wisdom.