<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Http on IT Comparison</title><link>https://comparison.metacog.co.kr/tags/http/</link><description>Recent content in Http on IT Comparison</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 02 Aug 2026 04:59:35 +0900</lastBuildDate><atom:link href="https://comparison.metacog.co.kr/tags/http/index.xml" rel="self" type="application/rss+xml"/><item><title>REST vs GraphQL: API Query Model</title><link>https://comparison.metacog.co.kr/posts/2026-08-02-rest-vs-graphql-api-query-model/</link><pubDate>Sun, 02 Aug 2026 04:59:35 +0900</pubDate><guid>https://comparison.metacog.co.kr/posts/2026-08-02-rest-vs-graphql-api-query-model/</guid><description>&lt;h2 id="overview"&gt;Overview&lt;/h2&gt;
&lt;p&gt;REST and GraphQL are both HTTP-based API paradigms that differ fundamentally in how clients request data. REST exposes multiple URLs with server-defined response shapes; GraphQL exposes a single endpoint where the client&amp;rsquo;s query body dictates exactly which fields and relationships to return. The distinction drives decisions around over-fetching, N+1 latency, HTTP cacheability, and schema contracts.&lt;/p&gt;
&lt;h2 id="comparison-diagram"&gt;Comparison Diagram&lt;/h2&gt;
&lt;div class="compare-diagram"&gt;
&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360"&gt;&lt;line x1="320" y1="8" x2="320" y2="352" style="stroke:var(--border)" stroke-width="1"/&gt;&lt;text x="160" y="26" text-anchor="middle" style="fill:var(--primary)" font-family="ui-monospace,monospace" font-size="17" font-weight="700"&gt;REST&lt;/text&gt;&lt;text x="160" y="43" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="10"&gt;multiple endpoints, fixed shape&lt;/text&gt;&lt;rect x="20" y="60" width="64" height="30" rx="4" style="fill:var(--compare-a-soft);stroke:var(--compare-a)" stroke-width="1.5"/&gt;&lt;text x="52" y="80" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="11"&gt;Client&lt;/text&gt;&lt;rect x="130" y="52" width="150" height="24" rx="3" style="fill:var(--compare-a-soft);stroke:var(--compare-a)" stroke-width="1"/&gt;&lt;text x="205" y="68" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="10"&gt;GET /users&lt;/text&gt;&lt;rect x="130" y="82" width="150" height="24" rx="3" style="fill:var(--compare-a-soft);stroke:var(--compare-a)" stroke-width="1"/&gt;&lt;text x="205" y="98" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="10"&gt;GET /posts&lt;/text&gt;&lt;rect x="130" y="112" width="150" height="24" rx="3" style="fill:var(--compare-a-soft);stroke:var(--compare-a)" stroke-width="1"/&gt;&lt;text x="205" y="128" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="10"&gt;GET /comments&lt;/text&gt;&lt;line x1="84" y1="70" x2="128" y2="64" style="stroke:var(--compare-a)" stroke-width="1.5"/&gt;&lt;line x1="84" y1="74" x2="128" y2="94" style="stroke:var(--compare-a)" stroke-width="1.5"/&gt;&lt;line x1="84" y1="78" x2="128" y2="124" style="stroke:var(--compare-a)" stroke-width="1.5"/&gt;&lt;text x="106" y="58" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;3 requests&lt;/text&gt;&lt;rect x="20" y="150" width="280" height="70" rx="4" style="fill:none;stroke:var(--border)" stroke-width="1" stroke-dasharray="4,3"/&gt;&lt;text x="28" y="166" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;Each response = full object:&lt;/text&gt;&lt;text x="28" y="182" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;{ id, name, email, avatarUrl,&lt;/text&gt;&lt;text x="28" y="196" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; createdAt, role, prefs }&lt;/text&gt;&lt;text x="28" y="212" style="fill:var(--compare-a)" font-family="ui-monospace,monospace" font-size="9" font-weight="600"&gt;only name+email needed → over-fetch&lt;/text&gt;&lt;rect x="20" y="232" width="280" height="26" rx="3" style="fill:var(--compare-a-soft);stroke:var(--compare-a)" stroke-width="1"/&gt;&lt;text x="160" y="250" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="10"&gt;Related data → N+1 round-trips&lt;/text&gt;&lt;rect x="20" y="266" width="280" height="26" rx="3" style="fill:none;stroke:var(--border)" stroke-width="1"/&gt;&lt;text x="160" y="284" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;Breaking change → new URL, e.g. /v2/&lt;/text&gt;&lt;text x="160" y="326" text-anchor="middle" style="fill:var(--compare-a)" font-family="ui-monospace,monospace" font-size="10" font-weight="600"&gt;GET · cacheable · status codes&lt;/text&gt;&lt;text x="480" y="26" text-anchor="middle" style="fill:var(--primary)" font-family="ui-monospace,monospace" font-size="17" font-weight="700"&gt;GraphQL&lt;/text&gt;&lt;text x="480" y="43" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="10"&gt;one endpoint, client-declared shape&lt;/text&gt;&lt;rect x="340" y="60" width="64" height="30" rx="4" style="fill:var(--compare-b-soft);stroke:var(--compare-b)" stroke-width="1.5"/&gt;&lt;text x="372" y="80" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="11"&gt;Client&lt;/text&gt;&lt;rect x="460" y="64" width="150" height="24" rx="3" style="fill:var(--compare-b-soft);stroke:var(--compare-b)" stroke-width="1.5"/&gt;&lt;text x="535" y="80" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="10"&gt;POST /graphql&lt;/text&gt;&lt;line x1="404" y1="75" x2="458" y2="75" style="stroke:var(--compare-b)" stroke-width="1.5"/&gt;&lt;text x="431" y="68" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;1 request&lt;/text&gt;&lt;rect x="340" y="100" width="140" height="110" rx="4" style="fill:none;stroke:var(--border)" stroke-width="1"/&gt;&lt;text x="410" y="115" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;query (client-authored)&lt;/text&gt;&lt;text x="348" y="133" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;query {&lt;/text&gt;&lt;text x="348" y="147" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; user(id: 1) {&lt;/text&gt;&lt;text x="348" y="161" style="fill:var(--compare-b)" font-family="ui-monospace,monospace" font-size="9" font-weight="600"&gt; name email&lt;/text&gt;&lt;text x="348" y="175" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; }&lt;/text&gt;&lt;text x="348" y="189" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; posts { title }&lt;/text&gt;&lt;text x="348" y="203" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;}&lt;/text&gt;&lt;rect x="490" y="100" width="126" height="110" rx="4" style="fill:none;stroke:var(--border)" stroke-width="1"/&gt;&lt;text x="553" y="115" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;SDL schema (server)&lt;/text&gt;&lt;text x="498" y="133" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;type User {&lt;/text&gt;&lt;text x="498" y="147" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; name: String!&lt;/text&gt;&lt;text x="498" y="161" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; email: String&lt;/text&gt;&lt;text x="498" y="175" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt; posts: [Post]&lt;/text&gt;&lt;text x="498" y="189" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;}&lt;/text&gt;&lt;rect x="340" y="220" width="276" height="40" rx="4" style="fill:var(--compare-b-soft);stroke:var(--compare-b)" stroke-width="1"/&gt;&lt;text x="478" y="235" text-anchor="middle" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;Response: exactly what was declared&lt;/text&gt;&lt;text x="348" y="252" style="fill:var(--content)" font-family="ui-monospace,monospace" font-size="9"&gt;{ user: { name, email }, posts: [{ title }] }&lt;/text&gt;&lt;rect x="340" y="266" width="276" height="26" rx="3" style="fill:none;stroke:var(--border)" stroke-width="1"/&gt;&lt;text x="478" y="284" text-anchor="middle" style="fill:var(--secondary)" font-family="ui-monospace,monospace" font-size="9"&gt;Schema evolves via @deprecated, no versioning&lt;/text&gt;&lt;text x="478" y="326" text-anchor="middle" style="fill:var(--compare-b)" font-family="ui-monospace,monospace" font-size="10" font-weight="600"&gt;POST · typed schema · introspectable&lt;/text&gt;&lt;/svg&gt;
&lt;/div&gt;
&lt;h2 id="comparison-table"&gt;Comparison Table&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;REST&lt;/th&gt;
&lt;th&gt;GraphQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Endpoints&lt;/td&gt;
&lt;td&gt;One URL per resource type — &lt;code&gt;GET /users&lt;/code&gt;, &lt;code&gt;POST /orders&lt;/code&gt;, etc.&lt;/td&gt;
&lt;td&gt;Single URL (&lt;code&gt;POST /graphql&lt;/code&gt;); resource selection is in the query body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response shape&lt;/td&gt;
&lt;td&gt;Fixed by the server; client receives all fields the endpoint defines&lt;/td&gt;
&lt;td&gt;Declared by the client per query; only the requested fields are returned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over/under-fetching&lt;/td&gt;
&lt;td&gt;Chronic: server returns full objects; client discards unused fields or must make extra requests for missing ones&lt;/td&gt;
&lt;td&gt;Eliminated by design: resolvers return only fields the query specifies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP caching&lt;/td&gt;
&lt;td&gt;Native: &lt;code&gt;GET&lt;/code&gt; responses cache at CDN and browser level by URL + headers&lt;/td&gt;
&lt;td&gt;Non-trivial: queries are &lt;code&gt;POST&lt;/code&gt; bodies; requires persisted queries or APQ for cache-key stability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type system&lt;/td&gt;
&lt;td&gt;Optional — enforced only if you add OpenAPI/JSON Schema; not validated at runtime by default&lt;/td&gt;
&lt;td&gt;Mandatory SDL schema; all queries are parsed and validated against it before execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;Breaking changes typically require new URL paths (&lt;code&gt;/v1/&lt;/code&gt;, &lt;code&gt;/v2/&lt;/code&gt;) or custom &lt;code&gt;Accept&lt;/code&gt; headers&lt;/td&gt;
&lt;td&gt;Additive schema evolution with &lt;code&gt;@deprecated&lt;/code&gt; directives; a single endpoint surface stays stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;HTTP status codes carry semantic meaning: 200, 404, 422, 500, etc.&lt;/td&gt;
&lt;td&gt;Always &lt;code&gt;200 OK&lt;/code&gt;; errors surface in an &lt;code&gt;errors[]&lt;/code&gt; array alongside any partial data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Introspection / discoverability&lt;/td&gt;
&lt;td&gt;Requires an external spec (OpenAPI/Swagger); not built into the protocol&lt;/td&gt;
&lt;td&gt;Built-in: query &lt;code&gt;__schema&lt;/code&gt; or &lt;code&gt;__type&lt;/code&gt; to get the full type graph at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="key-differences"&gt;Key Differences&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;REST has one endpoint per resource; GraphQL has one endpoint for the entire API, and the query body — not the URL — determines what data comes back.&lt;/li&gt;
&lt;li&gt;REST responses return all server-defined fields for a resource (over-fetch), and fetching related data requires additional round-trips (N+1 problem). A single GraphQL query can traverse multiple types and return exactly the fields requested.&lt;/li&gt;
&lt;li&gt;REST uses standard HTTP verbs and status codes, making GET-based CDN and browser caching trivially available. GraphQL queries travel as POST bodies, breaking HTTP cache semantics by default and requiring explicit workarounds.&lt;/li&gt;
&lt;li&gt;GraphQL&amp;rsquo;s SDL provides a mandatory, introspectable type contract enforced at parse time. REST type contracts are optional add-ons (OpenAPI); the server can return anything without violating the protocol.&lt;/li&gt;
&lt;li&gt;REST breaking changes typically force a new URL version (&lt;code&gt;/v2/&lt;/code&gt;); GraphQL prefers additive schema evolution with &lt;code&gt;@deprecated&lt;/code&gt;, keeping a single endpoint surface over the API&amp;rsquo;s lifetime.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="when-to-use-each"&gt;When to Use Each&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; — Use REST when HTTP semantics matter: public APIs where CDN caching, stateless &lt;code&gt;GET&lt;/code&gt;-based reads, and broad ecosystem compatibility (browsers, curl, API gateways) are first-class requirements. Also the natural default when each resource type is consumed independently and maps cleanly to standard CRUD verbs.&lt;/p&gt;</description></item></channel></rss>