REST API Design Components
Introduction
This page is here to remind me of what a good REST API looks like independent of language
REST API Design Considerations
This page outlines key architectural, operational, and maintainability concerns when designing and evolving REST APIs. Each section includes best practices, trade-offs, and onboarding notes for maintainers.
Security
- Use HTTPS for all communication.
- Implement authentication (e.g., OAuth2, JWT).
- Apply role-based authorization.
- Validate all inputs to prevent injection attacks.
- Avoid exposing internal models—use DTOs.
Caching
- Use `Cache-Control`, `ETag`, and `Last-Modified` headers.
- Cache GET responses where appropriate.
- Avoid caching sensitive or user-specific data.
- Consider CDN integration for static resources.
Resilience
Resilience patterns ensure that REST APIs remain reliable and responsive even when dependencies fail. It should be noted that this is for external resource used with the REST API, e.g. PostgreSQL, Redis, Keycloak. It is not for when Endpoints themselves fail.
Retry Policies
- Handle transient failures such as network hiccups or temporary service unavailability.
- Use exponential backoff with jitter to avoid thundering herds.
- Example: Database queries wrapped in
Policy.Handle<SqlException>().WaitAndRetryAsync(...). - Example: Keycloak OIDC config fetch wrapped in retry with correlationId logging.
Circuit Breakers
- Prevent cascading failures when a downstream service is consistently failing.
- Define thresholds (e.g., 5 failures in 30 seconds).
- Open circuit for a cooldown period, then half‑open to test recovery.
- Example: Redis connection attempts or external API calls.
Fallbacks
- Provide degraded but functional responses when dependencies fail.
- Return cached data or a default response.
- Log clearly that a fallback was used.
- Example: If Keycloak discovery fails, use last known signing keys until refresh succeeds.
Timeouts
- Prevent requests from hanging indefinitely.
- Apply per‑call timeouts (e.g., 2–5 seconds for external HTTP calls).
- Combine with retries and circuit breakers.
- Example: HttpClient calls to external APIs or database queries with command timeout.
Bulkhead Isolation
- Isolate resource pools so one failing dependency doesn’t exhaust all threads/connections.
- Use separate thread pools or connection pools per dependency.
- Limit concurrency per downstream service.
Observability Integration
- Ensure resilience events are visible in logs and metrics.
- Log retries, circuit breaker state changes, and fallbacks with correlationId.
- Expose metrics (retry count, circuit breaker open/closed) to Prometheus or OpenTelemetry.
Cross-Origin Resource Sharing (CORS)
CORS defines how browsers and APIs handle requests from different origins. Proper configuration is essential for both security and usability.
Purpose
- Allow controlled access to REST APIs from web applications hosted on different domains.
- Prevent unauthorized cross‑origin requests that could expose sensitive data.
Best Practices
- Restrict allowed origins to trusted domains (avoid using
*in production). - Limit allowed HTTP methods to those required (e.g.,
GET,POST). - Restrict allowed headers to only those necessary (e.g.,
Authorization,Content-Type). - Always enable HTTPS to protect credentials and tokens.
- Use short cache durations for preflight responses to reduce risk of stale policies.
Example Configuration
- In ASP.NET Core:
services.AddCors(options =>
{
options.AddPolicy("DefaultPolicy", builder =>
{
builder.WithOrigins("https://trustedapp.example.com")
.WithMethods("GET", "POST")
.WithHeaders("Authorization", "Content-Type");
});
});
- In Node.js (Express):
const cors = require('cors');
app.use(cors({
origin: 'https://trustedapp.example.com',
methods: ['GET','POST'],
allowedHeaders: ['Authorization','Content-Type']
}));
Observability
- Log rejected CORS requests for auditing.
- Monitor for unexpected origins attempting access.
- Integrate with security dashboards to detect misconfigurations.
Security Headers
Security headers protect REST APIs and clients against common web vulnerabilities by enforcing strict browser and HTTP behaviors.
Purpose
- Reduce attack surface by controlling how browsers handle responses.
- Prevent common exploits such as XSS, clickjacking, and MIME‑type sniffing.
Best Practices
- Always enable HTTPS and enforce HSTS (HTTP Strict Transport Security).
- Use
X-Content-Type-Options: nosniffto prevent MIME type sniffing. - Use
X-Frame-Options: DENYorSAMEORIGINto prevent clickjacking. - Use
Content-Security-Policyto restrict sources of scripts, styles, and other resources. - Use
Referrer-Policyto control how much referrer information is shared. - Use
Permissions-Policyto limit access to browser features (camera, microphone, geolocation).
Example Configuration
- In ASP.NET Core:
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Add("Referrer-Policy", "no-referrer");
context.Response.Headers.Add("Permissions-Policy", "geolocation=()");
await next();
});
- In Nginx:
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header Referrer-Policy "no-referrer";
add_header Permissions-Policy "geolocation=()";
Response Compression
Compression reduces payload size, improving performance and lowering bandwidth usage for REST APIs.
Purpose
- Improve API response times.
- Reduce network bandwidth consumption.
- Enhance client performance, especially on mobile or low‑bandwidth connections.
Best Practices
- Enable Gzip or Brotli compression for text‑based responses (JSON, XML, HTML).
- Avoid compressing already compressed formats (images, PDFs, ZIP files).
- Configure compression thresholds to skip very small responses.
- Ensure compression is applied only over HTTPS to avoid BREACH/CRIME vulnerabilities.
Example Configuration
- In ASP.NET Core:
services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/json" });
});
- In Nginx:
gzip on;
gzip_types application/json text/plain text/css application/javascript;
gzip_min_length 1024;
PUT vs PATCH
- PUT: Full resource replacement; idempotent (pure same everytime).
- PATCH: Partial update; not guaranteed idempotent (not pure could be different done twice).
- Prefer PATCH for user-facing updates; PUT for internal sync.
Code Coverage
- Use `sbt-scoverage` to measure coverage.
- Target 80%+ coverage for core logic.
- Exclude DTOs and boilerplate from metrics.
- Integrate with CI to enforce thresholds.
Unit Testing
- Test logic in isolation.
- Use mocks/stubs for external dependencies.
- Validate edge cases and error paths.
End-to-End (E2E) Testing
- Simulate full request/response lifecycle.
- Use real HTTP calls against a test server.
- Validate integration points and serialization.
Slugs
- Use slugs for human-readable URLs (e.g., `/films/the-godfather`).
- Ensure uniqueness and URL safety.
- Store slugs alongside primary keys.
Performance
- Paginate large datasets.
- Use filtering and sorting on server side.
- Profile endpoints with real-world data.
- Avoid N+1 queries and excessive joins.
Logging
- Log structured events (JSON preferred).
- Include request IDs and timestamps.
- Avoid logging sensitive data.
- Use log levels: DEBUG, INFO, WARN, ERROR.
Versioning
- Use URI-based versioning (`/v1/countries`) or header-based.
- Document breaking changes clearly.
- Deprecate old versions gracefully.
Error Modeling
- Use sealed trait + case classes for errors.
- Return appropriate HTTP status codes.
- Include machine-readable error codes and human-readable messages.
Documentation
- Use OpenAPI (Swagger) for endpoint specs.
- Include examples, error codes, and auth flows.
- Keep docs versioned and discoverable.
Observability
- Integrate metrics (e.g., Prometheus).
- Track latency, error rates, and throughput.
- Use tracing (e.g., OpenTelemetry) for distributed systems.
DTO Modeling
- Separate input/output DTOs from domain models.
- Use Play JSON formats in companion objects.
- Validate DTOs before mapping to domain.
Naming Conventions
- Use plural nouns for collections (`/countries`).
- Use kebab-case or snake_case consistently.
- Avoid verbs in endpoint paths.
Rate Limiting
- Protect endpoints from abuse.
- Use token buckets or leaky buckets.
- Return `429 Too Many Requests` with retry headers.
Pagination
- Use `limit` and `offset` or cursor-based paging.
- Return metadata: total count, next page token.
- Avoid over-fetching.
Monitoring
- Track uptime, latency, and error rates.
- Alert on anomalies.
- Use dashboards for visibility.
Onboarding Clarity
- Tag patterns and decisions in code.
- Maintain a glossary of terms and flows.
- Document legacy boundaries and migration paths.
Checklist
| Concern | Important | Best Practice | Done |
|---|---|---|---|
| Security | ✅ | ✅ | |
| Security Headers | ✅ | ✅ | |
| CORS | ✅ | ✅ | |
| Resilience | ✅ | ✅ | |
| Compression | ✅ | ✅ | |
| Caching | ✅ | ✅ | |
| PUT vs PATCH | ✅ | ✅ | |
| Code Coverage | ✅ | ✅ | |
| Unit Testing | ✅ | ✅ | |
| E2E Testing | ✅ | ✅ | |
| Slugs | ✅ | ✅ | |
| Performance | ✅ | ✅ | |
| Logging | ✅ | ✅ | |
| Versioning | ✅ | ✅ | |
| Error Modeling | ✅ | ✅ | |
| Documentation | ✅ | ✅ | |
| Observability | ✅ | ✅ | |
| DTO Modeling | ✅ | ✅ | |
| Naming Conventions | ✅ | ✅ | |
| Rate Limiting | ✅ | ✅ | |
| Pagination | ✅ | ✅ | |
| Monitoring | ✅ | ✅ | |
| Onboarding Clarity | ✅ | ✅ |
Tick off each item as you implement or validate it. This page is a living artifact—update it as your API evolves.