<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: charley-simon</title>
    <description>The latest articles on DEV Community by charley-simon (@charleysimon).</description>
    <link>https://dev.to/charleysimon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1298099%2F9ecb8c9c-ce4d-4ba6-9af2-06dbdfb97ecc.png</url>
      <title>DEV Community: charley-simon</title>
      <link>https://dev.to/charleysimon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/charleysimon"/>
    <language>en</language>
    <item>
      <title>Zero application code for a REST API — semantic navigation with LinkLab</title>
      <dc:creator>charley-simon</dc:creator>
      <pubDate>Tue, 14 Apr 2026 12:16:21 +0000</pubDate>
      <link>https://dev.to/charleysimon/zero-application-code-for-a-rest-api-semantic-navigation-with-linklab-2m9h</link>
      <guid>https://dev.to/charleysimon/zero-application-code-for-a-rest-api-semantic-navigation-with-linklab-2m9h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpihtaok4lncybgrc1gs2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpihtaok4lncybgrc1gs2.png" alt=" " width="800" height="866"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Zero application code for a REST API — semantic navigation with LinkLab
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;At the beginning of this project, I didn't know what storage I was going to keep.&lt;/p&gt;

&lt;p&gt;JSON first, because that's what TMDB returns and it's the native format of all the REST APIs I was consuming. Then MongoDB, to experiment — storing JSON is natural with a NoSQL database and I wanted that experience. Then Postgres, old habit, and because normalizing data seemed like the right direction.&lt;/p&gt;

&lt;p&gt;Every time I switched, I rewrote. Queries, routes, joins, links between resources. Not dramatic on a small project — but irritating enough to make me ask: should this really be my job?&lt;/p&gt;

&lt;p&gt;That frustration is where LinkLab was born.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;Before going further, let me be clear about something.&lt;/p&gt;

&lt;p&gt;If you read "abstraction over data", you probably thought ORM. Hibernate, Sequelize, ActiveRecord. I understand the reflex — and I share it. ORMs have cost me more time than they've saved. Lazy loading that blows up in production, N+1 discovered too late, generated SQL you can no longer control.&lt;/p&gt;

&lt;p&gt;LinkLab is not an ORM. It does not map tables to objects. It does not manage migrations. It does not hide your SQL.&lt;/p&gt;

&lt;p&gt;What it does: compile a navigation graph from your existing schema and resolve paths through it. The generated SQL is readable — you can see it live in the REPL. Nothing is hidden.&lt;/p&gt;

&lt;p&gt;An ORM hides your data behind objects. LinkLab exposes the relations between your entities as navigable paths. That's a fundamental difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's not all or nothing
&lt;/h2&gt;

&lt;p&gt;LinkLab does not require you to rewrite your project.&lt;/p&gt;

&lt;p&gt;You can point it at a single entity in your existing project, see what it finds on your real data, and decide from there. Nothing in your current code changes. If it brings value, you extend. If it doesn't fit your case, you've lost nothing.&lt;/p&gt;

&lt;p&gt;That's the best way to evaluate it: with your own data, not with examples.&lt;/p&gt;




&lt;h2&gt;
  
  
  The demo — PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Let's take a concrete database: dvdrental, the PostgreSQL demo database many devs know. 15 tables, classic relations, nothing artificial.&lt;/p&gt;

&lt;p&gt;Three commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;linklab init dvdrental
&lt;span class="c"&gt;# edit dvdrental.linklab.ts — set your connection details&lt;/span&gt;
linklab build dvdrental
linklab repl dvdrental
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build analyzes the schema, infers relations, compiles a graph of 210 routes. No manual join configuration. No mapping to write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;linklab v0.1.0  ·  graph

① Extract      ████████████  15 tables                     1229ms
② Analyze      ████████████  1 pivot · 3 warnings             5ms
③ Dictionary   ████████████  36 relations                      3ms
④ Assemble     ████████████  15 nodes · 36 edges               3ms
⑤ Train        ████████████  12 routes trained                 4ms
⑥ Compile      ████████████  210 routes                       36ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the REPL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;dvdrental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;film&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Academy Dinosaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What LinkLab generates automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
  &lt;span class="n"&gt;step0&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;film&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;film&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;film&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;ILIKE&lt;/span&gt; &lt;span class="s1"&gt;'Academy Dinosaur'&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;step1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;
    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;film_actor&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;film_actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;
    &lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;step0&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;step0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;film_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;film_actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;film_id&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10 actors, 93ms. The pivot table &lt;code&gt;film_actor&lt;/code&gt; was inferred automatically — you never mentioned it.&lt;/p&gt;

&lt;p&gt;The result is a plain JavaScript array. In the REPL, you can call &lt;code&gt;.map()&lt;/code&gt; directly on a Trail result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;dvdrental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;film&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Academy Dinosaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ['Guiness', 'Gable', 'Tracy'...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In application code, &lt;code&gt;await&lt;/code&gt; first and chain on the plain array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dvdrental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;film&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Academy Dinosaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ['Guiness', 'Gable', 'Tracy'...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Chaining &lt;code&gt;.filter().map()&lt;/code&gt; directly on a Trail result is on the roadmap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And chained navigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;dvdrental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;film&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Academy Dinosaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;film&lt;/span&gt;
  &lt;span class="err"&gt;↳&lt;/span&gt; &lt;span class="nx"&gt;film&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;film_actor&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;film_actor&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;film&lt;/span&gt;
  &lt;span class="mi"&gt;244&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;244 films. Two joins via &lt;code&gt;film_actor&lt;/code&gt;, traversal in both directions. One line.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tab completion
&lt;/h3&gt;

&lt;p&gt;What makes the difference in a live demo: the Tab key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;dvdrental&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;film&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Academy Dinosaur&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;actor&lt;/span&gt;  &lt;span class="nx"&gt;category&lt;/span&gt;  &lt;span class="nx"&gt;language&lt;/span&gt;  &lt;span class="nx"&gt;inventory&lt;/span&gt;  &lt;span class="nx"&gt;rental&lt;/span&gt;  &lt;span class="nx"&gt;payment&lt;/span&gt;  &lt;span class="nx"&gt;store&lt;/span&gt;  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The REPL doesn't show you all entities — it shows you the ones &lt;strong&gt;reachable from this context&lt;/strong&gt;. The graph speaking in real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same thing on JSON — and semantic views
&lt;/h2&gt;

&lt;p&gt;Let's switch source. I built a small test project around the TMDB API — films, people, credits stored as JSON files. Not a production app, just a playground to explore what LinkLab could do with a different data source.&lt;/p&gt;

&lt;p&gt;Same commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;linklab build cinema
linklab repl cinema
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;cinema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;278&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;people&lt;/span&gt;
  &lt;span class="mi"&gt;13&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tim Robbins, Morgan Freeman, Frank Darabont. Same navigation, same syntax — completely different source.&lt;/p&gt;

&lt;p&gt;But what's interesting here is something else.&lt;/p&gt;

&lt;p&gt;In this dataset, &lt;code&gt;movies&lt;/code&gt; and &lt;code&gt;people&lt;/code&gt; are linked by a &lt;code&gt;credits&lt;/code&gt; table. A credit is a person + a film + a role: actor, director, writer. It's a relation toward the same entity &lt;code&gt;people&lt;/code&gt; — differentiated by role.&lt;/p&gt;

&lt;p&gt;LinkLab detects this at compile time and automatically generates &lt;strong&gt;semantic views&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;cinema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;278&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;   &lt;span class="nx"&gt;actors&lt;/span&gt;   &lt;span class="nx"&gt;director&lt;/span&gt;   &lt;span class="nx"&gt;writers&lt;/span&gt;   &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;actors&lt;/code&gt;, &lt;code&gt;director&lt;/code&gt;, &lt;code&gt;writers&lt;/code&gt; are not distinct entities in your schema. They are paths toward &lt;code&gt;people&lt;/code&gt;, automatically filtered by role in &lt;code&gt;credits&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;cinema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;278&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;actors&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Tim&lt;/span&gt; &lt;span class="nx"&gt;Robbins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Morgan&lt;/span&gt; &lt;span class="nx"&gt;Freeman&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Bob&lt;/span&gt; &lt;span class="nx"&gt;Gunton&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="err"&gt;▸&lt;/span&gt; &lt;span class="nx"&gt;cinema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;278&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;director&lt;/span&gt;
  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;Frank&lt;/span&gt; &lt;span class="nx"&gt;Darabont&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's consistent here: &lt;code&gt;people('Christopher Nolan').director&lt;/code&gt; and &lt;code&gt;directors('Christopher Nolan')&lt;/code&gt; resolve to the same thing — same entity, filtered by role. No separate endpoint to maintain, no duplication.&lt;/p&gt;

&lt;p&gt;In a classic REST API, these would be separate endpoints, routes to maintain, queries to write. Here it's a graph — one path per intention.&lt;/p&gt;

&lt;p&gt;In application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;films&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cinema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;directors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Christopher Nolan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;movies&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;titles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;films&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;release_year&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// ['Interstellar', 'Inception', 'The Dark Knight'...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A REST API in one command
&lt;/h2&gt;

&lt;p&gt;The REPL is great for exploration. To expose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;linklab server cinema &lt;span class="nt"&gt;--expose-all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;LinkLab Server  ·  json:data
1532 compiled routes  ·  7 entities
URL  http://localhost:3000/api
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/api/movies/278/people
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;13 people. With their &lt;code&gt;_links&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tim Robbins"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_links"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"self"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/movies/278/people/504"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"up"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/movies/278"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"movies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/movies/278/people/504/movies"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"credits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/movies/278/people/504/credits"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Links are generated from the graph. Not configured — inferred. The client can navigate without knowing the API topology in advance. That's HATEOAS Level 3.&lt;/p&gt;

&lt;p&gt;Zero lines of application code. Zero routes written by hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  For production
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;linklab server&lt;/code&gt; command is for exploration and demos. For production, you plug &lt;code&gt;linklabPlugin&lt;/code&gt; directly into your own Fastify server, with your auth middleware, rate limiting, error handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fastify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fastify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;linklabPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@linklab/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Fastify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linklabPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compiledGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dataLoader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;postgresProvider&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;onEngine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access.check&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unauthenticated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What goes to production is the compiled graph and the plugin — not the CLI. The CLI is your development and exploration tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about sensitive data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;expose&lt;/code&gt; is set to &lt;code&gt;'none'&lt;/code&gt; — nothing in your database is accessible over HTTP without an explicit declaration. For this demo, we used &lt;code&gt;--expose-all&lt;/code&gt;. In a real project, you list exactly what you want to expose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;myproject&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;film&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;actor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensitive entities — users, payments, staff — stay invisible. &lt;code&gt;expose&lt;/code&gt; controls the surface, your &lt;code&gt;access.check&lt;/code&gt; hooks control per-user rights. Two separate concerns, both explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  There's a lot more to say
&lt;/h2&gt;

&lt;p&gt;This article shows the surface. What it doesn't show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL path optimization via Dijkstra's algorithm&lt;/li&gt;
&lt;li&gt;The weight system that learns from real usage and automatically recalibrates routes&lt;/li&gt;
&lt;li&gt;Real-time Trail observability via OpenTelemetry&lt;/li&gt;
&lt;li&gt;The view and action framework that sits on top of the graph&lt;/li&gt;
&lt;li&gt;Declarative filters in the Trail — &lt;code&gt;movies.where({ release_year: { gt: 2000 } })&lt;/code&gt; — under development, with a hook for cases the DSL doesn't cover natively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That will be the subject of future articles.&lt;/p&gt;




&lt;h2&gt;
  
  
  What LinkLab doesn't handle yet
&lt;/h2&gt;

&lt;p&gt;A few cases that may cause issues today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highly atypical schemas or schemas without clear naming conventions&lt;/li&gt;
&lt;li&gt;MongoDB — no driver yet, it's on the roadmap&lt;/li&gt;
&lt;li&gt;Databases with hundreds of tables — the build works but the graph becomes complex to explore&lt;/li&gt;
&lt;li&gt;Fine-grained per-resource authentication — possible via hooks but requires code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you hit a case that doesn't work, that's a GitHub issue. Not a disappointment — a contribution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it with your own data
&lt;/h2&gt;

&lt;p&gt;That's where it gets interesting. Not with dvdrental — with your own database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @linklabjs/cli
linklab init myproject
&lt;span class="c"&gt;# edit myproject.linklab.ts — set your connection details&lt;/span&gt;
linklab build myproject
linklab repl myproject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to rewrite your project. Point LinkLab at an existing schema, explore what it finds, and make up your own mind.&lt;/p&gt;

&lt;p&gt;The repo is on GitHub: &lt;strong&gt;&lt;a href="https://github.com/charley-simon/linklab" rel="noopener noreferrer"&gt;https://github.com/charley-simon/linklab&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Would you use something like this on a real project? And where do you think it would break?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>api</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
