VibeWeek
Home/Grow/GraphQL vs REST API Design: When Each Wins, How to Pick, and How to Ship Without Regret

GraphQL vs REST API Design: When Each Wins, How to Pick, and How to Ship Without Regret

⬅️ Day 6: Grow Overview

If you're building a SaaS in 2026 and standing up a public or internal API, the GraphQL-vs-REST decision is one of the few architectural choices that's both genuinely consequential and frequently made on vibes. Most founders default to REST because that's what they've seen, then hit problems six months in (over-fetching, N+1 queries to compose dashboard data, no schema enforcement). Or they jump to GraphQL because "it's modern," then suffer the operational reality at scale (caching is harder, authorization is per-resolver, query-complexity attacks are real).

A working API design decision answers: which model fits the consumers, the data shape, the team experience, and the operational maturity. Done well, your API is a load-bearing asset that scales with the business. Done badly, you're rewriting it in 18 months — usually from one paradigm to the other, hoping the new one solves what the old one didn't.

This guide is the implementation playbook for picking between REST and GraphQL (and the increasingly common "tRPC for internal; REST for public" hybrid), shipping the right one, and avoiding the mistakes each paradigm rewards.

The Honest Comparison: Where Each Wins

Forget the marketing. Here's what each is actually good at.

Help me compare REST vs GraphQL honestly.

The strengths matrix:

| Aspect | REST | GraphQL |
|---|---|---|
| Discoverability | Excellent (HTTP semantics; OpenAPI / Swagger) | Excellent (introspection; schema docs) |
| Caching | Easy (HTTP cache; CDN-friendly) | Harder (POST-based; needs APQ or client-side) |
| Versioning | Easy (URL versioning; v1, v2) | Schema evolution (deprecate fields; never break) |
| Over/under-fetching | Common (clients get what endpoint returns) | Avoided (clients pick exact fields) |
| Multiple-resource queries | N requests (N+1 from client) | One request (compose nested data) |
| Authorization | Per-endpoint (simple) | Per-resolver (complex; subtle bugs) |
| Tooling maturity | Decades | Strong (Apollo, urql, Relay) |
| Rate limiting | Per-endpoint (simple) | Per-query-cost (more complex) |
| File uploads | Easy (multipart) | Awkward (multipart workarounds) |
| Real-time | SSE / WebSocket | Subscriptions built-in |
| AI / LLM-friendly | Excellent (LLMs understand HTTP+JSON) | Good (schema introspection helps) |
| Public API | Excellent default | Smaller addressable market |
| Internal microservices | Good | Excellent (especially with codegen) |
| Mobile client | Over-fetches | Better (control bandwidth) |
| Single-page app | Good | Better (composable queries) |
| Server-rendered Next.js | Excellent | OK |

**Where REST wins outright**:

- Public APIs (developer audience expects HTTP)
- Caching matters (CDN; HTTP cache)
- Simple resource model (CRUD)
- Procurement / enterprise (REST is the default expectation)
- AI integrations (LLMs reason well about REST)
- File uploads / streaming responses

**Where GraphQL wins outright**:

- Complex multi-resource queries (avoid N+1 round-trips from client)
- Mobile clients (bandwidth matters)
- Long-running products with evolving data shape (no versioning)
- Subscriptions / real-time
- Internal service-to-service (with codegen)

**Where it''s a coin flip**:

- B2B SaaS dashboards
- Most internal APIs
- Mid-complexity domains

**The "what kind of clients do I have?" question**:

- One client (your own web app): either works
- Multiple clients (web + mobile + partners): GraphQL''s field-selection helps
- Public developer audience: REST default; GraphQL only if your audience is sophisticated

For my product:
- Number / type of clients
- Public or internal?
- Data-shape complexity
- Team experience with each

Output:
1. The honest assessment per aspect
2. The audience characterization
3. The "first instinct" pick
4. The reasons to override the instinct

The biggest unforced error: picking GraphQL because it''s "modern" without recognizing the operational complexity. GraphQL adds: per-resolver authorization, per-field rate limiting, query-complexity attacks, harder caching, harder error handling. If your team can''t honestly run all of that today, REST is the safer bet. Choose paradigms by the team you have, not the team you wish you had.

When REST Is the Right Default

For most indie SaaS in 2026, REST is the right default. Know when.

Help me decide if REST is right.

Pick REST when:

**1. Public-facing API**

Most developer audiences expect HTTP semantics:
- GET / POST / PUT / DELETE map to mental models
- Status codes (200, 201, 400, 401, 403, 404, 409, 422, 500) are universal
- curl-able / browser-debuggable

**2. AI / LLM integrations**

In 2026, AI agents are major API consumers (per [public-api](public-api-chat.md)). LLMs reason about REST APIs faster than GraphQL because:
- HTTP+JSON is in their training data
- OpenAPI specs are human-readable
- Endpoints are discrete actions
- Errors are status-code-typed

If your API is consumed by AI agents (MCP, custom integrations, function-calling), REST + OpenAPI is materially better.

**3. CDN caching matters**

REST endpoints are GET-cacheable at CDN. Public marketing data, blog data, public catalog data — perfect for edge cache.

GraphQL POST requests are typically not cached (some workarounds exist with APQ — Automatic Persisted Queries).

**4. Simple resource model**

If your domain is mostly CRUD on independent resources (users, projects, tasks), REST shines. The mapping is natural.

**5. Procurement / enterprise**

Enterprise procurement asks for "API access." REST is the assumed default. Asking enterprise IT to adopt GraphQL adds friction.

**6. Smaller team**

REST has lower operational complexity. If your engineering team is <10 and not specialized in API design, REST has a wider safety margin.

**7. File uploads / streaming**

Multipart file uploads work cleanly in REST. GraphQL has workarounds; they''re less native.

**The REST design principles**:

- Resources, not actions: `/users/123` not `/getUser?id=123`
- HTTP verbs: GET (read), POST (create), PUT (replace), PATCH (update), DELETE
- Status codes: meaningful per outcome
- Versioning: `/v1/...` URL or header
- Pagination: cursor-based (not offset for large data)
- Filtering: query params (`?status=active&limit=50`)
- Embedding: `?expand=author,tags` to reduce N+1
- Errors: consistent shape (per [public-api](public-api-chat.md))

**The "expand" pattern for N+1**:

GET /api/projects?expand=owner,tags

Returns projects with owner + tags inline; no N+1 from client


Solves the most common over-fetching complaint without adopting GraphQL.

For my system:
- Public vs internal
- AI consumer presence
- File-upload needs
- Team size + experience

Output:
1. The "REST is right" check
2. The REST-design conventions
3. The expand-pattern plan

The biggest REST mistake: endpoint-soup APIs. /getUser, /fetchUserList, /userCreate, /getUserStuff — verb-in-URL chaos. The fix: stick to resources + HTTP verbs. /users (list/create), /users/:id (get/update/delete). One pattern, applied consistently. Discipline beats cleverness.

When GraphQL Is the Right Choice

GraphQL has real wins. Know when those wins are worth the cost.

Help me decide if GraphQL is right.

Pick GraphQL when:

**1. Multiple clients with different data needs**

Web + mobile + partner integrations all need different slices of the same data.
- Web: full data
- Mobile: minimal data (bandwidth)
- Partner: middle ground

REST forces either over-fetching or many endpoints. GraphQL lets each client query exactly what it needs.

**2. Deeply nested / relational data**

Loading "project + tasks + assignees + comments" in one request is natural in GraphQL:

```graphql
query {
  project(id: "123") {
    name
    tasks {
      title
      assignee { name avatar }
      comments {
        body
        author { name }
      }
    }
  }
}

REST equivalent: 5+ requests, joined client-side, or a custom /projects/:id?expand=... pattern.

3. Long-lived product with evolving schema

GraphQL schema evolution: deprecate fields; clients keep working; no breaking changes.

REST: versioning at URL (/v1, /v2) or header. Either OK; both add overhead.

4. Subscriptions / real-time

GraphQL subscriptions are built-in. WebSocket-based. Native.

REST equivalent: SSE or separate WebSocket layer.

5. Sophisticated developer audience

If your API consumers are senior engineers who appreciate the typed schema, codegen, and field selection, GraphQL is rewarded.

If your audience is junior developers, AI agents, or "I just want to curl your API" — REST wins.

6. Internal microservices with codegen

GraphQL + Apollo Federation (or similar) makes internal API composition cleaner. Each service exposes a schema; gateway composes.

REST equivalent: pick conventions; less codegen; more wiring.

7. Mobile bandwidth-sensitive

Mobile apps over cellular benefit from minimal payload. Field selection saves bytes.

The GraphQL operational reality:

GraphQL adds these operational concerns:

Authorization complexity:

  • Each resolver needs to check permissions
  • "User can see project" might pass; "user can see project.budget" might fail
  • Field-level authorization is necessary; subtle bugs leak data

Query complexity / DoS:

Caching is harder:

  • Per-query caching (vs per-endpoint)
  • Persisted queries (APQ) help
  • Client-side caching (Apollo cache) helps

N+1 problems within GraphQL itself:

  • Naive resolvers cause N+1 to database
  • DataLoader pattern is mandatory at scale
  • Easy to ship a bad-perf GraphQL implementation

Error handling is awkward:

  • Errors in GraphQL responses are non-fatal (status 200; errors in body)
  • Mixing partial-success with HTTP-status thinking creates bugs
  • Apollo''s "error policy" needs to be agreed upon

Tooling:

  • Apollo Server / Yoga / Helix — Node.js servers
  • Apollo Federation / Hot Chocolate — multi-service
  • GraphQL Code Generator — TypeScript types from schema
  • Apollo Client / urql / Relay — clients
  • Pothos / Nexus / TypeGraphQL — code-first schema in TypeScript

For my system:

  • Multi-client need
  • Schema-evolution timeline
  • Operational maturity for resolver-level authorization

Output:

  1. The "GraphQL is right" check
  2. The operational-readiness check
  3. The migration / greenfield plan

The biggest GraphQL mistake: **shipping without DataLoader.** Naive resolvers fetch each field separately; loading 100 projects with assignees becomes 101 queries. DataLoader batches them. Mandatory at any scale. Without it, GraphQL is slower than REST.

## The Hybrid Approach: tRPC for Internal, REST for Public

A pattern increasingly common in 2026: tRPC inside the app; REST for external.

Help me decide on the hybrid approach.

The pattern:

Internal (frontend ↔ backend within your app): tRPC

  • Type-safe end-to-end (TypeScript types flow from server to client)
  • No code generation (just import types)
  • Procedure-based (not resource-based)
  • Built-in inference; minimal overhead
  • Works beautifully with Next.js / SvelteKit / Vercel Functions

External (public API for third parties): REST + OpenAPI

  • Standard HTTP semantics
  • Documented via OpenAPI
  • Cacheable
  • AI-agent-friendly
  • Works for non-TypeScript consumers

Why this works:

  • tRPC: optimized for internal speed-of-development with type safety
  • REST: optimized for external consumer compatibility

The two APIs serve different audiences:

  • tRPC consumers = your own web app
  • REST consumers = third-party developers, AI agents, integrations

The implementation:

// app/api/trpc/[trpc]/route.ts (Next.js)
// tRPC for the frontend

// app/api/v1/projects/route.ts
// REST for public API

Both implementations share:

  • Database queries
  • Domain logic
  • Authorization (extracted to shared functions)

When this is the right choice:

  • You build a React / Next.js / SvelteKit app
  • You also have a public API
  • TypeScript everywhere
  • Team comfortable with tRPC

When NOT to do this:

  • Multi-language internal (e.g., Go backend; React frontend) — tRPC requires TypeScript both sides
  • Single-API-only need — pick one
  • Team unfamiliar with tRPC — adoption cost

The "internal API also wants GraphQL" check:

If your internal frontend has highly-relational queries, tRPC works fine but isn''t as elegant as GraphQL. For a complex relational frontend, GraphQL might be the better internal choice.

For my system:

  • Internal vs external API audiences
  • Frontend stack
  • Public-API consumers

Output:

  1. The hybrid plan (tRPC + REST)
  2. The shared-logic structure
  3. The deploy / split decision

The biggest hybrid mistake: **trying to do both REST and GraphQL at the same time without a clear reason.** "We support both" usually means neither is well-maintained. Pick one as primary; offer the other only if there''s a specific consumer demanding it. Don''t maintain dual stacks for vague "flexibility."

## Decision Framework: Pick One in 5 Minutes

Stop deliberating. Use the framework.

Help me decide quickly.

The five-question decider:

1. Who consumes this API?

Consumer Tend toward
Public developers / AI agents REST
Your own React/Next app tRPC
Multiple clients (web + mobile + partner) GraphQL
Internal microservices GraphQL or REST
Enterprise procurement REST

2. What''s the data shape?

Shape Tend toward
CRUD on independent resources REST
Deeply nested / relational GraphQL
Mostly action-based (procedure calls) tRPC or REST
Complex aggregations GraphQL

3. Where will this be served from?

Hosting Tend toward
Vercel / serverless REST or tRPC
Traditional Node server Any
Edge functions only REST (cache-friendly)
Microservices with gateway GraphQL Federation

4. How mature is the team?

Team experience Tend toward
Junior / smaller team REST
TypeScript-heavy fullstack tRPC + REST hybrid
Senior / API-design-mature GraphQL
AI-agent-heavy product REST

5. How long will this API live?

Lifespan Tend toward
Short-lived / iterating fast REST (easier to deprecate)
5+ year horizon GraphQL (schema evolution helps)

The 90% answer:

For most indie SaaS in 2026:

  • External API: REST + OpenAPI
  • Internal frontend ↔ backend: tRPC (if Next.js / TypeScript)
  • Internal services: REST

GraphQL only if:

  • Multi-client (web + mobile + partner) AND
  • Complex relational data AND
  • Team can operate it (DataLoader, auth, cost analysis)

Don''t do:

  • GraphQL because "more modern"
  • GraphQL because "no versioning"
  • REST because "it''s simple" (without considering complex use cases)
  • Both REST and GraphQL on the same API surface
  • Switch paradigms without compelling reason

For my situation:

  • Run the 5 questions
  • The clear answer
  • The reasons to override

Output:

  1. The 5-question scorecard
  2. The pick
  3. The implementation outline

The biggest decision-process mistake: **deliberating for weeks on this choice.** It matters; it''s not a coin flip. But it''s also reversible at modest cost (3-6 months of migration work) at any scale below "millions of users." Pick; ship; revisit only if the choice is actively biting.

## Operational Discipline (Whichever You Pick)

Both paradigms reward discipline. Both punish neglect.

Help me set up operational discipline.

Common to both REST and GraphQL:

1. Authentication + authorization

  • Auth at gateway / middleware
  • Authorization close to data (per-resource for REST; per-resolver for GraphQL)
  • Tenant isolation enforced (per multi-tenancy-chat)

2. Rate limiting

3. Observability

4. Versioning

  • REST: URL versioning (/v1, /v2) — clean
  • GraphQL: deprecate fields; never remove
  • Communicate breaking changes clearly

5. Documentation

  • REST: OpenAPI / Swagger
  • GraphQL: introspection + Apollo Studio / GraphiQL
  • Examples per endpoint / query
  • Per public-api

6. Schema validation

  • REST: validate request body (Zod / Yup / Joi)
  • GraphQL: input types validated by schema
  • Reject malformed early

7. Caching

REST-specific discipline:

  • Status codes used correctly (404 for missing; 401 for auth; 403 for authz; 422 for validation)
  • ETag / If-None-Match for conditional requests
  • HATEOAS optional (most APIs skip; that''s OK)

GraphQL-specific discipline:

  • DataLoader for every relation
  • Query depth limit (e.g., max depth 7)
  • Query cost analysis (max cost 1000)
  • Persisted queries (APQ) for production clients
  • Field-level authorization checked
  • N+1 audits on schema changes

The "API governance" rule:

For any API change:

  • Backwards compatibility check
  • Documentation update
  • Changelog entry (per changelog-roadmap-chat)
  • Deprecation policy followed (typically 6-12 months)

The "test the API like a customer" rule:

Every endpoint should have:

  • Happy-path test
  • Auth-fail test
  • Authz-fail test
  • Validation-fail test
  • Tenant-isolation test (different tenant cannot access)

Multi-tenant isolation tests are non-negotiable.

For my system:

  • The discipline gaps
  • The fixes prioritized

Output:

  1. The discipline checklist
  2. The gap audit
  3. The fix plan

The biggest operational mistake: **shipping the API without per-endpoint metrics.** A slow endpoint is invisible until customers complain. A breaking change is invisible until partners email. Per-endpoint latency / error rate / usage metrics are mandatory; without them, the API is a black box. Add them on day one, not month six.

## Don''t Over-Engineer the Decision

A reasonable choice executed well beats a perfect choice executed poorly.

Help me avoid over-engineering.

Don''t:

  • Spend 6 weeks deciding (the choice matters; not THAT much)
  • Mock both and benchmark micro-perf (irrelevant at indie scale)
  • Read 20 blog posts arguing one way (they argue forever)
  • Adopt both "to keep options open" (operational nightmare)
  • Switch paradigms 18 months in for "cleanliness" (rarely justified)

Do:

  • Pick based on customer / data / team — fast
  • Ship a working API in 2 weeks
  • Measure real usage; adjust if reality contradicts the bet
  • Trust that competent execution > paradigm-purity

The "good enough" benchmark:

Your API is good enough if:

  • Customers can do what they need
  • New endpoints / fields ship in <1 day for typical changes
  • Latency is acceptable (p95 < 500ms typical)
  • Errors are observable and actionable
  • You''re not spending 50% of engineering on API plumbing

The "we got it wrong" signal:

Real signals to revisit:

  • Multi-client over-fetching is causing customer complaints (might want GraphQL)
  • AI agents struggle to use the API (might want REST)
  • Versioning causing pain across many endpoints (might want GraphQL)
  • GraphQL N+1 / DoS attacks happening (might want stricter controls or hybrid)

NOT signals:

  • New shiny paradigm released
  • Engineer joined who likes the other one
  • "Modern stack" envy

The "switch later" cost:

Switching paradigms costs 3-12 months of engineering. It''s reversible but expensive. Make the first pick well; only switch if reality is actually biting.

For my decision:

  • The pick (committed)
  • The ship plan (2 weeks)
  • The "we got it wrong" signal definition

Output:

  1. The committed pick
  2. The MVP API
  3. The signal-monitoring plan

The biggest decision mistake: **spending more time on the decision than on building.** The teams that ship great APIs aren''t the ones that picked perfectly; they''re the ones that picked reasonably and executed well. A REST API with good docs, consistent patterns, and observability beats a GraphQL API with poor resolvers and broken auth — and vice versa. Pick fast; build well.

---

## What "Done" Looks Like

A working API design decision in 2026 has:

- Clear pick (REST / GraphQL / tRPC + REST hybrid) made in <1 week
- Pick justified by audience + data shape + team experience
- API documented (OpenAPI for REST; introspection + docs for GraphQL)
- Auth + authorization at the right layer
- Per-endpoint / per-query metrics
- Rate limiting (per-endpoint or per-cost)
- Tenant isolation tested
- Versioning strategy in place
- Team comfortable shipping new endpoints / queries fast

The hidden cost of weak API design: **rework you can''t avoid.** The wrong paradigm bites at scale: GraphQL teams with no DataLoader hit DB-meltdown; REST teams with N+1 client requests hit user-perceived slowness; both teams with weak auth hit data leaks. The cost shows up as engineering time spent re-platforming instead of shipping. Pick deliberately; execute disciplined; revisit only on real signal.

## See Also

- [Public API](public-api-chat.md) — productizing the API
- [API Versioning](api-versioning-chat.md) — versioning strategy
- [API Keys](api-keys-chat.md) — auth for public API
- [Rate Limiting & Abuse](rate-limiting-abuse-chat.md) — protecting the API
- [Multi-Tenancy](multi-tenancy-chat.md) — tenant isolation
- [Caching Strategies](caching-strategies-chat.md) — response caching
- [Database Indexing Strategy](database-indexing-strategy-chat.md) — query speed
- [Outbound Webhooks](outbound-webhooks-chat.md) — push complement
- [Inbound Webhooks](inbound-webhooks-chat.md) — receiving events
- [Real-Time Collaboration](real-time-collaboration-chat.md) — subscriptions / WebSocket
- [Performance Optimization](performance-optimization-chat.md) — broader perf
- [Service Level Agreements](service-level-agreements-chat.md) — API SLAs
- [Audit Logs](audit-logs-chat.md) — API access trail
- [VibeReference: API Gateway Providers](https://www.vibereference.com/backend-and-data/api-gateway-providers) — gateway layer
- [VibeReference: Database Providers](https://www.vibereference.com/backend-and-data/database-providers) — DB backing API
- [VibeReference: API](https://www.vibereference.com/backend-and-data/api) — API fundamentals
- [VibeReference: OpenAPI](https://www.vibereference.com/backend-and-data/openapi) — REST documentation
- [VibeReference: Swagger](https://www.vibereference.com/backend-and-data/swagger) — REST documentation
- [LaunchWeek: Activation Metric Definition](https://www.launchweek.com/4-convert/activation-metric-definition) — events tracked through API

[⬅️ Day 6: Grow Overview](README.md)