Principles
Minimal surface: Expose only what's needed. Every endpoint is attack surface.
Explicit over implicit: Access control visible, not buried in queries.
Fail closed: Unexpected = deny.
REST patterns
Resource-based URLs: Easier to apply consistent auth.
Filter response fields: Use DTOs. Never return raw DB objects.
Pagination: Always. Unbounded lists = data exfiltration. Max per_page server-side.
Rate limiting: Per endpoint, per user. Stricter for auth endpoints.
Consistent errors: Don't leak info. "Invalid credentials" not "user not found" vs "wrong password".
GraphQL
Disable introspection in production.
Depth and complexity limits: Without them, nested queries cause exponential DB work.
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== "production",
validationRules: [depthLimit(5), createComplexityLimitRule(1000)],
});
Per-field authorisation: Single /graphql endpoint - auth must be at resolver level.
email: (parent, args, context) => {
if (context.user.id !== parent.id && !context.user.isAdmin) return null;
return parent.email;
}
gRPC
TLS always - even between internal services.
Authenticate via metadata - validate in interceptors.
Message size limits: grpc-go defaults to 4 MiB on receive. Lower it for endpoints that handle small payloads: grpc.MaxRecvMsgSize(1 * 1024 * 1024).
The takeaway
Minimal surface, explicit access control, fail closed. Resource-based URLs, filter responses, paginate, rate limit. GraphQL: disable introspection, limit depth, per-field auth. gRPC: TLS, metadata auth, size limits.
