<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://woohongseok.github.io/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://woohongseok.github.io/blog/" rel="alternate" type="text/html" /><updated>2026-04-02T05:15:06+00:00</updated><id>https://woohongseok.github.io/blog/feed.xml</id><title type="html">woohongseok</title><subtitle>woohongseok&apos;s dev blog</subtitle><entry><title type="html">Strip the Framework Away</title><link href="https://woohongseok.github.io/blog/backend/2026/04/02/strip-the-framework-away.html" rel="alternate" type="text/html" title="Strip the Framework Away" /><published>2026-04-02T03:00:00+00:00</published><updated>2026-04-02T03:00:00+00:00</updated><id>https://woohongseok.github.io/blog/backend/2026/04/02/strip-the-framework-away</id><content type="html" xml:base="https://woohongseok.github.io/blog/backend/2026/04/02/strip-the-framework-away.html"><![CDATA[<p>In my <a href="/blog/backend/2026/04/01/every-http-framework-solves-the-same-problem.html">previous post</a>, I compared how Go, Express, and Spring handle the same endpoint. I found a four-stage pipeline that every framework follows: route matching, request parsing, business logic, response serialization.</p>

<p>Today I want to go one level deeper. Not what the stages are, but what they reveal about frameworks themselves.</p>

<h2 id="two-kinds-of-code">Two kinds of code</h2>

<p>Look at those four stages again. Three of them — route matching, request parsing, response serialization — are HTTP plumbing. They exist because HTTP speaks text, and your code speaks objects. Someone has to translate.</p>

<p>The fourth — business logic — is yours. It’s the reason the server exists. Find user 42. Check if the email is taken. Calculate the total.</p>

<p>No framework can write that part for you, because it changes with every application. But the plumbing? That’s the same every time. Parse JSON, match a URL, serialize a response. Repetitive work that never changes regardless of what you’re building.</p>

<p>That’s what a framework is. It automates the repetitive HTTP plumbing so you can focus on the part that actually matters — your business logic.</p>

<h2 id="the-automation-spectrum">The automation spectrum</h2>

<p>But frameworks don’t all automate the same amount. Here’s what I found when I mapped out who does what:</p>

<p><strong>Go (net/http)</strong> — route matching only. You parse the request body yourself with <code class="language-plaintext highlighter-rouge">json.Decoder</code>. You serialize the response yourself with <code class="language-plaintext highlighter-rouge">json.Encoder</code>. You check HTTP methods with an if-statement.</p>

<p><strong>Express</strong> — route matching and response serialization. <code class="language-plaintext highlighter-rouge">app.get()</code> handles both method and path. <code class="language-plaintext highlighter-rouge">res.json()</code> serializes for you. But request parsing? You need to register <code class="language-plaintext highlighter-rouge">express.json()</code> middleware yourself. It’s not on by default.</p>

<p><strong>Spring</strong> — everything. <code class="language-plaintext highlighter-rouge">@GetMapping</code> scans and registers routes at startup. <code class="language-plaintext highlighter-rouge">@RequestBody</code> deserializes JSON into a typed object. Returning an object from a handler automatically serializes it. You write a method, add some annotations, and the framework handles the rest.</p>

<p>Three tools, same four stages, different levels of automation.</p>

<h2 id="opinionated-vs-unopinionated">Opinionated vs unopinionated</h2>

<p>This difference has a name.</p>

<p>Spring is <strong>opinionated</strong>. It assumes you’ll use JSON. It assumes you want automatic type conversion. It assumes convention over configuration. The framework makes decisions for you.</p>

<p>Express is <strong>unopinionated</strong>. It doesn’t assume you want JSON parsing — maybe you’re handling XML, or form data, or raw binary. It gives you the pieces and lets you assemble them.</p>

<p>Go’s standard library isn’t even a framework. It gives you the bare minimum: a way to listen for requests and send responses. Everything else is your problem.</p>

<h2 id="the-trade-off">The trade-off</h2>

<p>More automation means more convenience. Spring lets you write a three-line handler and get a fully working endpoint. You ship faster. You write less boilerplate.</p>

<p>But more automation also means less flexibility. When Spring auto-serializes your response, you don’t control the JSON field order, or how nulls are handled, or whether empty arrays become <code class="language-plaintext highlighter-rouge">[]</code> or get omitted. You can configure these things, but now you’re fighting the defaults instead of just writing code.</p>

<p>Express sits in the middle. It helps where you ask it to, stays out of the way otherwise. You get convenience where you want it and control where you need it.</p>

<p>Go gives you nothing you didn’t ask for. Verbose, but transparent. When something breaks, there’s no magic to debug — just your code.</p>

<h2 id="what-i-actually-learned">What I actually learned</h2>

<p>The insight isn’t that Spring is better or Go is purer. It’s that the pipeline is constant:</p>

<p><strong>Route matching → Request parsing → Business logic → Response serialization.</strong></p>

<p>Every HTTP framework is just a different answer to one question: how much of that pipeline should be automated?</p>

<p>When I pick up a new framework now, I don’t memorize syntax. I ask: where on the spectrum does this sit? What does it automate, and what does it leave to me? The answer tells me almost everything I need to know about how it will feel to work with.</p>]]></content><author><name></name></author><category term="backend" /><summary type="html"><![CDATA[In my previous post, I compared how Go, Express, and Spring handle the same endpoint. I found a four-stage pipeline that every framework follows: route matching, request parsing, business logic, response serialization.]]></summary></entry><entry><title type="html">Every HTTP Framework Solves the Same Problem</title><link href="https://woohongseok.github.io/blog/backend/2026/04/01/every-http-framework-solves-the-same-problem.html" rel="alternate" type="text/html" title="Every HTTP Framework Solves the Same Problem" /><published>2026-04-01T03:00:00+00:00</published><updated>2026-04-01T03:00:00+00:00</updated><id>https://woohongseok.github.io/blog/backend/2026/04/01/every-http-framework-solves-the-same-problem</id><content type="html" xml:base="https://woohongseok.github.io/blog/backend/2026/04/01/every-http-framework-solves-the-same-problem.html"><![CDATA[<p>I’ve been learning backend development by comparing how different frameworks handle the same task. Today I built a simple <code class="language-plaintext highlighter-rouge">GET /users/:id</code> endpoint in three frameworks — Go’s <code class="language-plaintext highlighter-rouge">net/http</code>, Express, and Spring — and found something interesting.</p>

<p>They all do the same thing. The difference is just how much they do for you.</p>

<h2 id="the-experiment">The experiment</h2>

<p>Here’s the same endpoint in three frameworks.</p>

<p><strong>Go:</strong></p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/users/"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">r</span><span class="o">.</span><span class="n">Method</span> <span class="o">!=</span> <span class="s">"GET"</span> <span class="p">{</span>
        <span class="n">w</span><span class="o">.</span><span class="n">WriteHeader</span><span class="p">(</span><span class="m">405</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    <span class="n">id</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Path</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="s">"/users/"</span><span class="p">)</span><span class="o">:</span><span class="p">]</span>
    <span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span><span class="s">"id"</span><span class="o">:</span> <span class="n">id</span><span class="p">})</span>
<span class="p">})</span>
</code></pre></div></div>

<p><strong>Express:</strong></p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/users/:id</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="nx">findUser</span><span class="p">(</span><span class="nb">Number</span><span class="p">(</span><span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">user</span><span class="p">)</span> <span class="k">return</span> <span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">(</span><span class="mi">404</span><span class="p">).</span><span class="nx">json</span><span class="p">({</span> <span class="na">error</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Not found</span><span class="dl">'</span> <span class="p">});</span>
    <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">user</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p><strong>Spring:</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GetMapping</span><span class="o">(</span><span class="s">"/users/{id}"</span><span class="o">)</span>
<span class="kd">public</span> <span class="nc">User</span> <span class="nf">getUser</span><span class="o">(</span><span class="nd">@PathVariable</span> <span class="nc">Long</span> <span class="n">id</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">return</span> <span class="n">userRepository</span><span class="o">.</span><span class="na">findById</span><span class="o">(</span><span class="n">id</span><span class="o">)</span>
        <span class="o">.</span><span class="na">orElseThrow</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="k">new</span> <span class="nc">ResponseStatusException</span><span class="o">(</span><span class="nc">HttpStatus</span><span class="o">.</span><span class="na">NOT_FOUND</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>

<p>At first they look completely different. Annotations, callbacks, raw if-statements. But when I traced what actually happens at runtime, a common structure showed up.</p>

<h2 id="the-four-stage-pipeline">The four-stage pipeline</h2>

<p>Every HTTP framework processes a request through the same four stages:</p>

<ol>
  <li><strong>Route matching</strong> — find which handler should process this request.</li>
  <li><strong>Request parsing</strong> — extract parameters, convert types.</li>
  <li><strong>Business logic</strong> — do the actual work.</li>
  <li><strong>Response serialization</strong> — turn the result into JSON and send it.</li>
</ol>

<p>That’s it. No framework skips any of these steps. The only question is: who does each step?</p>

<h2 id="who-does-what">Who does what</h2>

<p><strong>Route matching</strong></p>

<p>Go only matches the path. It doesn’t care about HTTP methods at all — you check <code class="language-plaintext highlighter-rouge">r.Method</code> yourself with an if-statement. Express matches both method and path through <code class="language-plaintext highlighter-rouge">app.get()</code>. Spring scans your classes at startup, finds <code class="language-plaintext highlighter-rouge">@GetMapping</code> annotations, and registers routes automatically.</p>

<p>Same problem, three levels of automation.</p>

<p><strong>Request parsing</strong></p>

<p>In Go, the path parameter doesn’t even exist as a concept. You slice the URL string yourself: <code class="language-plaintext highlighter-rouge">r.URL.Path[len("/users/"):]</code>. Express gives you <code class="language-plaintext highlighter-rouge">req.params.id</code>, but it’s always a string — you call <code class="language-plaintext highlighter-rouge">Number()</code> yourself. Spring sees <code class="language-plaintext highlighter-rouge">@PathVariable Long id</code> and converts <code class="language-plaintext highlighter-rouge">"42"</code> to <code class="language-plaintext highlighter-rouge">Long 42</code> automatically.</p>

<p>Again, same problem. Go makes you do everything, Express does half, Spring does all of it.</p>

<p><strong>Response serialization</strong></p>

<p>Go: you create a JSON encoder and write to the response writer manually. Express: you call <code class="language-plaintext highlighter-rouge">res.json()</code>. Spring: you just <code class="language-plaintext highlighter-rouge">return</code> an object and the framework serializes it.</p>

<p><strong>Business logic</strong></p>

<p>This is the one row where all three frameworks are identical: you write it yourself. No framework can know that you want to find user 42 in a database. Routing, parsing, serialization — that’s all plumbing. Frameworks can automate plumbing. They can’t automate your business logic.</p>

<h2 id="the-pattern-behind-the-pattern">The pattern behind the pattern</h2>

<table>
  <thead>
    <tr>
      <th>Stage</th>
      <th>Go</th>
      <th>Express</th>
      <th>Spring</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Route matching</td>
      <td>path only, manual method check</td>
      <td>method + path</td>
      <td>annotation scan</td>
    </tr>
    <tr>
      <td>Request parsing</td>
      <td>manual string slicing</td>
      <td><code class="language-plaintext highlighter-rouge">req.params</code> (string)</td>
      <td>auto type conversion</td>
    </tr>
    <tr>
      <td>Business logic</td>
      <td>you write it</td>
      <td>you write it</td>
      <td>you write it</td>
    </tr>
    <tr>
      <td>Response serialization</td>
      <td>manual encoding</td>
      <td><code class="language-plaintext highlighter-rouge">res.json()</code></td>
      <td>auto on return</td>
    </tr>
  </tbody>
</table>

<p>The columns go from left to right: raw → convenient → automated. This maps directly to a concept called <strong>Inversion of Control</strong>. In Go, you control everything. In Express, you control most things but the framework handles routing and provides helpers. In Spring, the framework controls the flow — it finds your code, calls it, converts types, and serializes responses. You just declare what you want with annotations.</p>

<p>The trade-off is clear. More automation means less boilerplate but more magic. Go gives you nothing you didn’t ask for. Spring gives you everything, whether you understand it or not.</p>

<h2 id="what-i-took-away">What I took away</h2>

<p>When I see a new framework now, I don’t try to memorize its API. I ask four questions:</p>

<ol>
  <li>How does it register routes?</li>
  <li>How does it parse requests?</li>
  <li>Where do I put my logic?</li>
  <li>How does it build responses?</li>
</ol>

<p>Every HTTP framework is just a different answer to these four questions. The pipeline is always the same.</p>]]></content><author><name></name></author><category term="backend" /><summary type="html"><![CDATA[I’ve been learning backend development by comparing how different frameworks handle the same task. Today I built a simple GET /users/:id endpoint in three frameworks — Go’s net/http, Express, and Spring — and found something interesting.]]></summary></entry><entry><title type="html">REST Principles: From Chaos to Structure</title><link href="https://woohongseok.github.io/blog/backend/2026/03/31/rest-principles-from-chaos-to-structure.html" rel="alternate" type="text/html" title="REST Principles: From Chaos to Structure" /><published>2026-03-31T03:00:00+00:00</published><updated>2026-03-31T03:00:00+00:00</updated><id>https://woohongseok.github.io/blog/backend/2026/03/31/rest-principles-from-chaos-to-structure</id><content type="html" xml:base="https://woohongseok.github.io/blog/backend/2026/03/31/rest-principles-from-chaos-to-structure.html"><![CDATA[<h2 id="what-is-a-resource">What is a Resource?</h2>

<p>When you build an API, the URL should point to a thing, not an action. That thing is called a resource.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/users</code> → the user collection</li>
  <li><code class="language-plaintext highlighter-rouge">/users/3</code> → the specific user with ID 3</li>
</ul>

<p>You might be tempted to name your endpoints <code class="language-plaintext highlighter-rouge">/getUsers</code> or <code class="language-plaintext highlighter-rouge">/createUser</code>. I did that too. But the problem is that you’re baking the action into the URL. REST says: let the URL just describe what you’re talking about. The how part? That’s someone else’s job.</p>

<h2 id="uri-design-rules">URI Design Rules</h2>

<p>A few conventions that keep URIs sane:</p>

<ul>
  <li>Use nouns: <code class="language-plaintext highlighter-rouge">/users</code>, <code class="language-plaintext highlighter-rouge">/posts</code>, <code class="language-plaintext highlighter-rouge">/comments</code></li>
  <li>Plural for collections: <code class="language-plaintext highlighter-rouge">/users</code> not <code class="language-plaintext highlighter-rouge">/user</code></li>
  <li>Nest for relationships: <code class="language-plaintext highlighter-rouge">/users/3/posts</code> — posts that belong to user 3</li>
  <li>No verbs: <code class="language-plaintext highlighter-rouge">/users</code>, never <code class="language-plaintext highlighter-rouge">/getUsers</code></li>
</ul>

<p>Think of a URI as a street address. It tells you where something is, not what to do with it.</p>

<h2 id="http-method-meaning">HTTP Method Meaning</h2>

<p>So if the URL doesn’t say what to do, what does? The HTTP method.</p>

<ul>
  <li><strong>GET</strong> — read</li>
  <li><strong>POST</strong> — create</li>
  <li><strong>PUT</strong> — update</li>
  <li><strong>DELETE</strong> — remove</li>
</ul>

<p>They all work on the same URL:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET    /users     → list all users
POST   /users     → create a user
PUT    /users/3   → update user 3
DELETE /users/3   → delete user 3
</code></pre></div></div>

<p>Here’s something worth knowing: idempotency. If you accidentally send the same request twice, what happens?</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">PUT /users/3</code> with <code class="language-plaintext highlighter-rouge">{ name: "Alice" }</code> twice → still Alice. Same result. That’s idempotent.</li>
  <li><code class="language-plaintext highlighter-rouge">POST /users</code> with <code class="language-plaintext highlighter-rouge">{ name: "Alice" }</code> twice → now you have two Alices. Not idempotent.</li>
</ul>

<p>GET, PUT, and DELETE are idempotent. POST is not. This actually matters — when the network is flaky and a request fires twice, idempotent methods are safe to retry. POST needs extra care.</p>

<h2 id="status-code-classification">Status Code Classification</h2>

<p>When the server responds, it sends back a number. You’ve seen 404 on broken web pages. That number is the status code, and it follows a simple pattern by the hundreds digit:</p>

<ul>
  <li><strong>1xx</strong> — Informational</li>
  <li><strong>2xx</strong> — Success</li>
  <li><strong>3xx</strong> — Redirect</li>
  <li><strong>4xx</strong> — Client messed up</li>
  <li><strong>5xx</strong> — Server messed up</li>
</ul>

<p>You don’t need to memorize all of them. Just a handful comes up over and over:</p>

<ul>
  <li><strong>200</strong> — all good</li>
  <li><strong>201</strong> — all good, and something new was created (typically after POST)</li>
  <li><strong>204</strong> — all good, but there’s nothing to send back (typically after DELETE)</li>
  <li><strong>400</strong> — bad request, something’s wrong with what you sent</li>
  <li><strong>404</strong> — that resource doesn’t exist</li>
  <li><strong>500</strong> — something broke on the server side</li>
</ul>

<p>The point is: the client can understand the result from the number alone, before even reading the body.</p>

<h2 id="hateoas">HATEOAS</h2>

<p>This one sounds fancy. HATEOAS — Hypermedia As The Engine Of Application State.</p>

<p>The idea is simple: what if API responses included links, like a website does? You land on a page, you see links, you click one and go somewhere new. What if APIs worked the same way?</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
  </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Alice"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"links"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"posts"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/users/3/posts"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"delete"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/users/3"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The client wouldn’t need to know URLs in advance. Just follow the links.</p>

<p>Sounds great, right? In practice, almost nobody does this. Frontend developers already know exactly which endpoints to hit — they hardcode those calls for each screen. Parsing links from every response just to figure out where to go next adds complexity for no real gain. Elegant idea, rarely worth it.</p>

<h2 id="richardson-maturity-model">Richardson Maturity Model</h2>

<p>There’s a model that grades how RESTful your API is, from Level 0 to Level 3.</p>

<p><strong>Level 0</strong> — One URL, one method. Everything goes through a single endpoint.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /api  { "action": "getUsers" }
POST /api  { "action": "deleteUser", "id": 3 }
</code></pre></div></div>

<p>Brutal, but some APIs actually look like this.</p>

<p><strong>Level 1</strong> — You split things into separate resources. <code class="language-plaintext highlighter-rouge">/users</code>, <code class="language-plaintext highlighter-rouge">/posts</code>. But you’re still using POST for everything.</p>

<p><strong>Level 2</strong> — Now you’re using the right HTTP methods and returning proper status codes. GET for reading, POST for creating, 201 for created, 404 for not found. This is where things start feeling right.</p>

<p><strong>Level 3</strong> — HATEOAS. Responses include links. The full REST vision.</p>

<hr />

<p>Most APIs in the wild sit at Level 2, and that’s perfectly fine. Level 3 is more academic than practical. When people say an API is “RESTful,” they usually mean it uses resources, proper HTTP methods, and meaningful status codes.</p>

<p>One last thing: frameworks like Express or Spring Boot don’t make your API RESTful. They give you the tools — <code class="language-plaintext highlighter-rouge">app.get</code>, <code class="language-plaintext highlighter-rouge">app.post</code>, route parameters. But nothing stops you from writing <code class="language-plaintext highlighter-rouge">app.get('/deleteUser', ...)</code>. REST is a design choice you make, not something a framework enforces.</p>

<p>REST is not the only way. There are alternatives like GraphQL, WebSocket, and SSE, each solving problems REST doesn’t handle well.</p>]]></content><author><name></name></author><category term="backend" /><summary type="html"><![CDATA[What is a Resource?]]></summary></entry></feed>