<?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: Wade Thomas</title>
    <description>The latest articles on DEV Community by Wade Thomas (@wadethomastt).</description>
    <link>https://dev.to/wadethomastt</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%2F3850437%2F8d98205a-0b4e-4e70-abeb-0bb759317abf.jpg</url>
      <title>DEV Community: Wade Thomas</title>
      <link>https://dev.to/wadethomastt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wadethomastt"/>
    <language>en</language>
    <item>
      <title>What is PostgreSQL? The Database That Powers Modern Web Applications</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Tue, 26 May 2026 15:28:23 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-postgresql-the-database-that-powers-modern-web-applications-4l83</link>
      <guid>https://dev.to/wadethomastt/what-is-postgresql-the-database-that-powers-modern-web-applications-4l83</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/qPm6Fa2G_gQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this video we are going to talk about PostgreSQL — what it is, why it is one of the most popular databases in the world, and why it is the database I use to power my web applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;PostgreSQL — often called Postgres — is a free, open-source relational database management system. It has been in active development for over 35 years and is widely considered one of the most advanced, stable and feature-rich databases available.&lt;br&gt;
A relational database stores data in tables — rows and columns — and uses SQL to query and manipulate that data. If you have ever worked with a spreadsheet, you already understand the basic concept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ACID compliance
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is fully ACID compliant — Atomicity, Consistency, Isolation, Durability. Transactions either complete fully or not at all. If something goes wrong mid-transaction, the database rolls back to the previous state. For financial data, orders, user accounts — anything where data integrity matters — this is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced data types
&lt;/h3&gt;

&lt;p&gt;PostgreSQL supports JSON and JSONB for document storage, arrays, UUID, geometric types, full-text search and more. You get the flexibility of a document database with the reliability of a relational database.&lt;br&gt;
Excellent performance&lt;br&gt;
PostgreSQL handles complex queries, large datasets and high concurrency extremely well. It has a sophisticated query planner and optimizer that makes even complex joins and aggregations fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensibility
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is highly extensible. PostGIS for geospatial data, pgvector for AI embeddings, and TimescaleDB for time series data are just a few examples of powerful extensions available.&lt;br&gt;
Open source and free&lt;br&gt;
PostgreSQL is completely free and open source with no licensing costs. There is no enterprise tier required to access advanced features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Widely supported
&lt;/h3&gt;

&lt;p&gt;Almost every major framework, ORM, and tool supports PostgreSQL. Directus, Prisma, Drizzle, Sequelize, Django, Rails — they all work with PostgreSQL out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL vs MySQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is more standards compliant and supports more advanced features out of the box. MySQL is slightly simpler to set up and has historically been faster for simple read-heavy workloads. For modern web applications with complex data requirements, PostgreSQL is generally the better choice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Chose PostgreSQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is the database that Directus recommends and works best with. It gives me full ACID compliance, advanced data types, and excellent performance — everything I need for production web applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;PostgreSQL is a battle-tested, feature-rich, open-source relational database that powers some of the world's largest applications. For modern web development it is one of the safest and most capable choices available.&lt;br&gt;
In an upcoming video we will deploy PostgreSQL alongside Directus on our VPS using Coolify.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• PostgreSQL Website
• PostgreSQL Documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backenddevelopment</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What is Directus? The Headless CMS That Sits On Your Own Database</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Sun, 17 May 2026 22:17:59 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-directus-the-headless-cms-that-sits-on-your-own-database-10d</link>
      <guid>https://dev.to/wadethomastt/what-is-directus-the-headless-cms-that-sits-on-your-own-database-10d</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/83OJERORAj8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Before we get into setting up Coolify, I want to make sure you understand the tools we will be deploying. In this video we are going to talk about Directus — what it is, what it does, and why I chose it as the backbone of my back-end stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Directus?
&lt;/h2&gt;

&lt;p&gt;Directus is a free, open-source, headless CMS and data platform. But before we go any further, let me explain what headless CMS means.&lt;/p&gt;

&lt;p&gt;A traditional CMS — like WordPress — couples the content management system with the front end. The way your content is stored and the way it is displayed are tightly linked. You are locked into how WordPress presents your content.&lt;/p&gt;

&lt;p&gt;A headless CMS separates the two. Directus manages and stores your data, and exposes it through a REST API or GraphQL endpoint. Your front end — whether it is a React app, a mobile app, or anything else — consumes that API and decides how to display the content. The CMS has no head — meaning no fixed front end — hence the name headless.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes Directus Different?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It works with your existing database
&lt;/h3&gt;

&lt;p&gt;Most headless CMS platforms use their own proprietary data storage. Directus sits on top of a standard relational database — PostgreSQL, MySQL, SQLite and more. Your data is stored in plain database tables that you own and can access directly. You are never locked into a proprietary format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-generated API
&lt;/h3&gt;

&lt;p&gt;When you create a collection in Directus — think of a collection like a database table — it automatically generates a full REST API and GraphQL endpoint for that collection. No code required. You get full CRUD operations out of the box — create, read, update and delete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Powerful admin dashboard
&lt;/h3&gt;

&lt;p&gt;Directus comes with a beautiful, fully featured admin dashboard out of the box. Your clients or content editors can manage content without ever touching code. You can customize the dashboard with custom fields, relationships, file uploads, translations and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  Role-based access control
&lt;/h3&gt;

&lt;p&gt;Directus has a very granular permissions system. You can control exactly who can read, create, update or delete data at the collection level, the field level, and even the row level. This makes it suitable for multi-tenant applications and complex permission requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  File management
&lt;/h3&gt;

&lt;p&gt;Directus includes a full file and asset management system. You can upload images, videos, documents and more. It supports on-the-fly image transformations — resize, crop, format conversion — all through URL parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flows — built-in automation
&lt;/h3&gt;

&lt;p&gt;Directus has a built-in automation system called Flows. You can build workflows triggered by events — like sending an email when a new order is created, or updating a related record when a status changes — all without writing code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open source and self-hostable
&lt;/h3&gt;

&lt;p&gt;Directus is completely open source. You can self-host it on your own server, which means your data stays on your infrastructure. There is also a cloud hosted option if you prefer a managed solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Chose Directus
&lt;/h2&gt;

&lt;p&gt;I chose Directus for several reasons.&lt;/p&gt;

&lt;p&gt;First, it works with PostgreSQL out of the box. I wanted a standard relational database that I own and control, not a proprietary data store.&lt;/p&gt;

&lt;p&gt;Second, the auto-generated API saves me an enormous amount of time. Instead of building CRUD endpoints for every collection, Directus handles that automatically. I focus on building features, not boilerplate API code.&lt;/p&gt;

&lt;p&gt;Third, the admin dashboard is genuinely excellent. My clients can manage their own content without any technical knowledge. I do not have to build a custom admin interface for every project.&lt;/p&gt;

&lt;p&gt;Fourth, it is self-hostable. My data stays on my server. I control the infrastructure, the costs, and the data.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the Limitations?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not a traditional backend framework&lt;/strong&gt; — complex business logic may require supplementing with custom code or choosing a different solution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Licensing costs at scale&lt;/strong&gt; — Directus is free and open source for projects generating up to &lt;strong&gt;$5 million USD&lt;/strong&gt; in annual revenue. Beyond that threshold a commercial license is required. For the vast majority of startups, small teams and indie developers this limit will never be reached, making it effectively free for most use cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can be overkill for simple projects&lt;/strong&gt; — a basic blog may not need all of Directus's features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Directus is a powerful, flexible, open-source headless CMS that sits on top of your own database and gives you a full API and admin dashboard out of the box. For developers building modern web applications who want to own their data and move fast without writing boilerplate, it is one of the best tools available.&lt;/p&gt;

&lt;p&gt;In an upcoming video we will deploy Directus on our VPS using Coolify and connect it to our TanStack Start front end.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://directus.io" rel="noopener noreferrer"&gt;Directus Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.directus.io" rel="noopener noreferrer"&gt;Directus Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/directus/directus" rel="noopener noreferrer"&gt;Directus GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>directus</category>
      <category>headlesscms</category>
      <category>opensource</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>What is Coolify? Self-Hosting with Superpowers</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Thu, 14 May 2026 03:40:29 +0000</pubDate>
      <link>https://dev.to/wadethomastt/what-is-coolify-self-hosting-with-superpowers-o01</link>
      <guid>https://dev.to/wadethomastt/what-is-coolify-self-hosting-with-superpowers-o01</guid>
      <description>&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/oFmJYMk1iCg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the last video, we talked about the VPS and why it is a compelling option for hosting your web applications. I mentioned a tool called Coolify that makes managing a VPS significantly easier. In this video, we are going to dive deeper into what Coolify actually is, what it does, and why I think it is one of the best tools available for developers and small teams who want the power of a VPS without the complexity of managing one from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Coolify?
&lt;/h2&gt;

&lt;p&gt;Coolify is a free, open-source, self-hostable platform as a service — or PaaS. Think of it as your own personal Heroku or Render, but running on your own server. This means you own your infrastructure, your data, and your costs.&lt;/p&gt;

&lt;p&gt;The best way to understand Coolify is to compare it to the alternatives. Platforms like Heroku, Render, and Railway are fully managed PaaS solutions. They abstract away all the server complexity — you push your code and it runs. The trade-off is cost and control. As your app scales, the bills grow quickly and you have limited control over the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;Coolify gives you the same developer experience — push your code and it deploys — but on a VPS that you control. You get the simplicity of a managed platform with the economics and control of a VPS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does Coolify Do?
&lt;/h2&gt;

&lt;p&gt;Coolify handles all the hard parts of running applications on a VPS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git Integration
&lt;/h3&gt;

&lt;p&gt;Connect your GitHub, GitLab, or Bitbucket repository and Coolify will automatically deploy your app every time you push to your main branch. No manual deployments, no SSH commands — just push your code and it is live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dockerized Deployments
&lt;/h3&gt;

&lt;p&gt;Every application Coolify deploys runs in a Docker container. This means your apps are isolated, portable, and consistent across environments. You do not need to know Docker deeply to use Coolify — it handles the containerization for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic HTTPS
&lt;/h3&gt;

&lt;p&gt;Coolify integrates with Let's Encrypt to automatically provision and renew SSL certificates for all your applications. Every app gets HTTPS out of the box with zero configuration on your part.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in Reverse Proxy
&lt;/h3&gt;

&lt;p&gt;Coolify uses Traefik as its built-in reverse proxy and web server. It automatically routes traffic to the right application based on the domain name. You can run multiple applications on the same VPS and Coolify handles the routing between them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database Management
&lt;/h3&gt;

&lt;p&gt;Coolify can deploy and manage databases alongside your applications — PostgreSQL, MySQL, MongoDB, Redis and more. You can spin up a database with a few clicks and connect it to your application without any manual configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;Manage your environment variables securely through the Coolify dashboard. No more manually editing &lt;code&gt;.env&lt;/code&gt; files on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring and Logs
&lt;/h3&gt;

&lt;p&gt;Coolify provides basic monitoring and real-time log streaming for all your applications directly from the dashboard. You can see what your app is doing without SSH-ing into the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backups
&lt;/h3&gt;

&lt;p&gt;Coolify supports automated database backups to S3-compatible storage. Your data is protected without any manual backup scripts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Would You Use Coolify?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  You want the economics of a VPS without the complexity
&lt;/h3&gt;

&lt;p&gt;A $6 to $10 per month VPS with Coolify can run multiple applications that would cost hundreds of dollars per month on Heroku, Render, or Railway. For a startup or indie developer this is a significant saving.&lt;/p&gt;

&lt;h3&gt;
  
  
  You want full control over your infrastructure
&lt;/h3&gt;

&lt;p&gt;With Coolify you own everything. Your data stays on your server. You choose your hosting provider. You are not locked into any platform's pricing or terms of service.&lt;/p&gt;

&lt;h3&gt;
  
  
  You want a great developer experience
&lt;/h3&gt;

&lt;p&gt;Coolify's dashboard is clean and intuitive. Deploying an application is genuinely just a few clicks. It does not feel like managing a server — it feels like using a modern PaaS.&lt;/p&gt;

&lt;h3&gt;
  
  
  You are running multiple projects
&lt;/h3&gt;

&lt;p&gt;One VPS with Coolify can host multiple applications, multiple databases, and multiple domains. Instead of paying for separate hosting for each project, you consolidate everything onto one server.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are the Limitations?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are responsible for your server&lt;/strong&gt; — if your VPS goes down, your apps go down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some configuration is still required&lt;/strong&gt; — especially for custom setups, firewalls, and advanced networking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is self-hosted&lt;/strong&gt; — meaning you need to keep Coolify itself updated and maintained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not ideal for very large scale&lt;/strong&gt; — for enterprise applications with massive traffic you may need dedicated infrastructure beyond a single VPS.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Do You Get Started?
&lt;/h2&gt;

&lt;p&gt;Getting Coolify up and running is surprisingly straightforward. In an upcoming video I will walk you through the complete setup — from provisioning a VPS to having Coolify installed and your first application deployed.&lt;/p&gt;

&lt;p&gt;All you need to get started is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A VPS with at least &lt;strong&gt;2GB RAM&lt;/strong&gt; and &lt;strong&gt;2 CPU cores&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A domain name&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;30 minutes&lt;/strong&gt; of your time&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Coolify bridges the gap between the simplicity of managed platforms and the power and economics of a VPS. For developers and small teams who want to own their infrastructure without being overwhelmed by server management, it is genuinely one of the best tools available right now.&lt;/p&gt;

&lt;p&gt;In an upcoming video we will get our hands dirty and set up Coolify from scratch. See you there.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://coolify.io" rel="noopener noreferrer"&gt;Coolify Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coolify.io/docs" rel="noopener noreferrer"&gt;Coolify Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer"&gt;Coolify GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>coolify</category>
      <category>vps</category>
      <category>docker</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Back-End Web Development — VPS vs Vercel vs Netlify</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Tue, 12 May 2026 01:14:25 +0000</pubDate>
      <link>https://dev.to/wadethomastt/back-end-web-development-vps-vs-vercel-vs-netlify-3cok</link>
      <guid>https://dev.to/wadethomastt/back-end-web-development-vps-vs-vercel-vs-netlify-3cok</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;🎬 This article is a companion to my YouTube video. Watch it here:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/jxMMyRrWcyk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every great web application with millions of users has an even greater back end — and it has to be. Accessing your app over a prolonged period of time will test the integrity, availability, and speed of your back end. Backend technology can be inexpensive, or it can cost you thousands of dollars. So what's the deal here?&lt;/p&gt;

&lt;p&gt;Let me warn you that this channel is highly opinionated. I am sharing the tools that I use and how I use them.&lt;/p&gt;

&lt;p&gt;For my back end that powers my web apps, I use &lt;strong&gt;Directus&lt;/strong&gt; for my headless CMS, supported by a &lt;strong&gt;PostgreSQL&lt;/strong&gt; and &lt;strong&gt;Redis&lt;/strong&gt; database, hosted on a &lt;strong&gt;VPS&lt;/strong&gt;, and managed by &lt;strong&gt;Coolify&lt;/strong&gt;. Wow, that is a mouthful — and in this series we will explore how they all work together.&lt;/p&gt;

&lt;p&gt;So let's tackle each technology one at a time, starting with the VPS.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a VPS?
&lt;/h2&gt;

&lt;p&gt;A Virtual Private Server, known as a VPS, is a virtual environment created on a physical server. Think of it like an apartment building — everyone shares the same physical structure, but each unit has its own private space, utilities, and security.&lt;/p&gt;

&lt;p&gt;While multiple VPS instances share a physical server, be careful not to confuse this with shared hosting. Each VPS is allocated its own dedicated resources, which are restricted to that VPS for as long as it is active. Therefore, the performance of a VPS is not directly affected by the usage of other VPS instances, but rather by the underlying performance of the physical server itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What are the Benefits of a VPS?
&lt;/h2&gt;

&lt;p&gt;There are several benefits to consider when deciding on virtual private server hosting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greater control&lt;/strong&gt; — Compared to shared hosting, you have root access and can fully customize your server environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated resources&lt;/strong&gt; — Allocated CPU, memory, and storage help ensure consistent performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — You can easily scale resources up or down to accommodate changing traffic and application demands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-effectiveness&lt;/strong&gt; — A VPS typically offers a balance between the affordability of shared hosting and the power of a dedicated server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved security&lt;/strong&gt; — Isolation from other users on the same physical server enhances security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choice of operating system&lt;/strong&gt; — You can choose the operating system that best suits your needs, such as Linux or Windows.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How Does it Stack Up Against Vercel or Netlify?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vercel
&lt;/h3&gt;

&lt;p&gt;Vercel offers minimal setup with Git integration and fast deployment right out of the box. It has a free Hobby plan, but it is strictly for personal, non-commercial use. Once your app grows and needs to scale, you will need to move to a paid plan.&lt;/p&gt;

&lt;p&gt;The Pro plan starts at &lt;strong&gt;$20 per user per month&lt;/strong&gt;, and also covers serverless functions. Despite the name, serverless code still runs on a physical server — however, the infrastructure management, such as scaling, security patching, and provisioning, is handled entirely by the cloud provider.&lt;/p&gt;

&lt;p&gt;The main factor to watch carefully is cost. Vercel uses usage-based billing, and costs can escalate quickly — especially since Turbo build machines became the default for new Pro projects in February 2026, at &lt;strong&gt;$0.126 per build minute&lt;/strong&gt;. A moderate team workflow can generate over &lt;strong&gt;$400 per month&lt;/strong&gt; in build costs alone, before bandwidth and compute charges. I have seen some alarming Vercel bills shared online by customers, which means you need to fully understand the pricing model before you commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Netlify
&lt;/h3&gt;

&lt;p&gt;Netlify has historically been more suited to static sites and composable web applications. Composable applications are software systems built from independent, interchangeable modules rather than a single rigid codebase. Netlify also has Git integration, customizable build plugins, and serverless functions.&lt;/p&gt;

&lt;p&gt;Netlify moved to a credit-based pricing model in September 2025, designed to simplify metered billing. As of April 2026, the Pro plan costs &lt;strong&gt;$20 per month&lt;/strong&gt; and now includes unlimited team member seats, which is an improvement over the previous per-seat model. However, teams working on active projects with real traffic can burn through their credit allocation quickly, and credit pack add-ons at &lt;strong&gt;$10 per 1,500 credits&lt;/strong&gt; mean teams can regularly spend $40 to $80 or more per month beyond the base subscription. Your overall control is also more limited compared to a VPS, and you will still face a monthly cost as your app grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  The VPS
&lt;/h3&gt;

&lt;p&gt;VPS prices can grow as your app grows, but in my experience at a slower and more predictable rate — with no surprise charges. However, if you manage a VPS yourself, it requires understanding web servers, firewalls, operating systems, caching, and more. It can get complex very quickly and may cause you to spend more time managing the server than building and scaling your app.&lt;/p&gt;

&lt;p&gt;That is why some developers and business owners hire a team to handle it. Small teams may not have the budget for a dedicated infrastructure team, so they often opt for a more convenient managed solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coolify — The Game Changer
&lt;/h2&gt;

&lt;p&gt;There is one tool that I think is worth mentioning and is a genuine game changer — and that is &lt;strong&gt;Coolify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Coolify is a self-hostable platform that makes managing your VPS significantly easier. It offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Git integration&lt;/li&gt;
&lt;li&gt;✅ Dockerized container deployments&lt;/li&gt;
&lt;li&gt;✅ Automatic HTTPS&lt;/li&gt;
&lt;li&gt;✅ Built-in web server with no manual configuration required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the VPS a very real and competitive option worth considering. If you choose a solid hosting provider and pair it with the right tools, you can make this an absolute dream to work with.&lt;/p&gt;

&lt;p&gt;Now don't get me wrong — even with Coolify there are some configurations you will have to make. But I do believe a VPS is a great solution for a startup, mid-size, or enterprise scaled app with the right tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Stay with me in this series and we will definitely explore the possibilities. See you in the next video.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/learn/what-is-a-virtual-private-server" rel="noopener noreferrer"&gt;Google Cloud — What is a VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/what-is/vps/" rel="noopener noreferrer"&gt;AWS — What is a VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/think/topics/vps" rel="noopener noreferrer"&gt;IBM — VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.scalahosting.com/blog/what-is-a-vps-technical-explanation/" rel="noopener noreferrer"&gt;Scala Hosting — VPS Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.dreamhost.com/blog/beginners-guide-vps/" rel="noopener noreferrer"&gt;DreamHost — Beginner's Guide to VPS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/pricing" rel="noopener noreferrer"&gt;Vercel Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.netlify.com/pricing/" rel="noopener noreferrer"&gt;Netlify Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bejamas.com/blog/self-hosting-vs-vercel-and-netlify-which-solution-is-right" rel="noopener noreferrer"&gt;Bejamas — Self Hosting vs Vercel and Netlify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;🔔 Subscribe to my YouTube channel for the full series on building a modern web app back end from scratch.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>coolify</category>
      <category>netlify</category>
      <category>vercel</category>
    </item>
    <item>
      <title>Protecting Routes in TanStack Start with Zustand</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:54:07 +0000</pubDate>
      <link>https://dev.to/wadethomastt/protecting-routes-in-tanstack-start-with-zustand-2ci9</link>
      <guid>https://dev.to/wadethomastt/protecting-routes-in-tanstack-start-with-zustand-2ci9</guid>
      <description>&lt;p&gt;Route protection in TanStack Start doesn't have a built-in auth guard out of the box, but combining a Zustand store with a layout route gives you a clean, reusable solution that works well with SSR and hydration.&lt;/p&gt;

&lt;p&gt;Here's the full setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Auth Store
&lt;/h2&gt;

&lt;p&gt;First, the Zustand store. It persists the user to localStorage via the &lt;code&gt;persist&lt;/code&gt; middleware and uses a &lt;code&gt;hasHydrated&lt;/code&gt; flag to track when the store has been rehydrated from storage. This is critical — without it you'll get a flash of redirect on page load even for logged-in users.&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="kd"&gt;type&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;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;create&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;zustand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;persist&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;zustand/middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;logoutUser&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;@/lib/directus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AuthState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setHasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;:&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="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAuthStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AuthState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;
  &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;get&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="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;setHasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;set&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="na"&gt;logout&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;logoutUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;isLoggedIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nf"&gt;get&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="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auth-storage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;onRehydrateStorage&lt;/span&gt;&lt;span class="p"&gt;:&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;state&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;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;setHasHydrated&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="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;The &lt;code&gt;onRehydrateStorage&lt;/code&gt; callback fires once Zustand has finished reading from localStorage and sets &lt;code&gt;hasHydrated&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This is the signal your protected layout waits for before making any redirect decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The User Type
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; type maps directly to the Directus user fields you need in your app.&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;location&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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;h2&gt;
  
  
  The Protected Layout Route
&lt;/h2&gt;

&lt;p&gt;TanStack Start's file-based routing lets you create layout routes that wrap child routes. The &lt;code&gt;_protectedLayout&lt;/code&gt; route acts as an auth guard — it checks the store and redirects to login if no user is present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MainLayout&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;@/components/layouts/MainLayout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useNavigate&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;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;useAuthStore&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;@/store/authStore&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;useEffect&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/_protectedLayout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProtectedLayout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;notFoundComponent&lt;/span&gt;&lt;span class="p"&gt;:&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;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;This page doesn't exist!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProtectedLayout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&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="nx"&gt;hasHydrated&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAuthStore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="nx"&gt;hasHydrated&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&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;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasHydrated&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;hasHydrated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MainLayout&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two guard conditions are important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;if (!hasHydrated) return null&lt;/code&gt; — waits for the store to rehydrate before rendering anything, preventing a flash of redirect&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if (!user) return null&lt;/code&gt; — prevents rendering the layout while the redirect is in flight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only when both conditions pass does &lt;code&gt;MainLayout&lt;/code&gt; render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Layout
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MainLayout&lt;/code&gt; is a standard shell component that renders your header, footer and the current route's content via &lt;code&gt;Outlet&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Outlet&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;@tanstack/react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&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;../Header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;Footer&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;../Footer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;MainLayout&lt;/span&gt;&lt;span class="p"&gt;()&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-7xl mx-auto p-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Outlet&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Footer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Any route nested under &lt;code&gt;_protectedLayout&lt;/code&gt; automatically gets the auth guard and the shared layout — no need to add protection logic to individual routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It All Fits Together
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;User logs in → &lt;code&gt;setUser&lt;/code&gt; is called → Zustand persists the user to localStorage&lt;/li&gt;
&lt;li&gt;User revisits the app → Zustand rehydrates from localStorage → &lt;code&gt;hasHydrated&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Protected layout checks &lt;code&gt;hasHydrated&lt;/code&gt; and &lt;code&gt;user&lt;/code&gt; → renders &lt;code&gt;MainLayout&lt;/code&gt; if both pass&lt;/li&gt;
&lt;li&gt;User logs out → &lt;code&gt;logout&lt;/code&gt; calls Directus, clears the user from the store → redirect to login&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;This pattern keeps auth logic in one place, works cleanly with SSR hydration, and scales to as many protected routes as you need without repeating any guard logic.&lt;/p&gt;

&lt;p&gt;Have questions about the setup? Drop a comment below.&lt;/p&gt;

</description>
      <category>tanstack</category>
      <category>zustand</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Directus Auth out of the Box — Registration, Login, Email Verification and Password Reset</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:37:13 +0000</pubDate>
      <link>https://dev.to/wadethomastt/directus-auth-out-of-the-box-registration-login-email-verification-and-password-reset-5dba</link>
      <guid>https://dev.to/wadethomastt/directus-auth-out-of-the-box-registration-login-email-verification-and-password-reset-5dba</guid>
      <description>&lt;p&gt;One of the things I appreciate most about Directus is that authentication is built in. Registration, login, email verification, and password reset — all handled without reaching for a third party auth service like Auth0 or Clerk.&lt;/p&gt;

&lt;p&gt;Here's how I implement the full auth flow in a TanStack Start app using the Directus SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  User Registration
&lt;/h2&gt;

&lt;p&gt;The SDK's &lt;code&gt;registerUser&lt;/code&gt; function handles registration. You pass the email, password and any additional fields. The &lt;code&gt;verification_url&lt;/code&gt; tells Directus where to redirect the user after they click the link in their email — Directus appends the token to that URL automatically.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;registerUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verification_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_TANSTACK_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/verify-email`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;registerUserDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;Directus sends the verification email automatically via your configured SMTP provider. I use Resend — SMTP is configured directly in the Docker Compose file via environment variables and the whole setup takes just a few minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;EMAIL_TRANSPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smtp.resend.com&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_PORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;465&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resend&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_SMTP_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_resend_api_key&lt;/span&gt;
&lt;span class="na"&gt;EMAIL_FROM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;noreply@yourdomain.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Login
&lt;/h2&gt;

&lt;p&gt;Logging in is a single call on the Directus client. Session cookies are handled automatically thanks to the &lt;code&gt;credentials: 'include'&lt;/code&gt; config set up on the client.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loginUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&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;Clean, no token management, no localStorage juggling. The session is maintained via cookie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email Verification
&lt;/h2&gt;

&lt;p&gt;When the user clicks the link in their verification email, they land on your &lt;code&gt;/auth/verify-email&lt;/code&gt; route with a &lt;code&gt;token&lt;/code&gt; in the query string. You take that token and hit Directus's verify endpoint directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;VerifyEmailComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;token&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verifying&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/users/register/verify-email?token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="nf"&gt;verify&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="nx"&gt;token&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;The &lt;code&gt;status&lt;/code&gt; state drives your UI — show a spinner while verifying, a success message on completion, or an error state if the token is invalid or expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  Password Reset
&lt;/h2&gt;

&lt;p&gt;The password reset flow works the same way — Directus emails the user a reset link with a token, they land on your reset page, and you POST the new password along with the token to Directus's reset endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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="nx"&gt;password&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Passwords do not match.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&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="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/auth/password/reset`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/auth/login&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to reset password. The link may have expired.&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Something went wrong. Please try again.&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="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;A 204 response means success — redirect the user to login. Handle expired tokens gracefully with a clear error message.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Directus Handles For You
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Sending the verification email — via your SMTP provider configured in Docker Compose&lt;/li&gt;
&lt;li&gt;✅ Generating and validating tokens&lt;/li&gt;
&lt;li&gt;✅ Sending the password reset email&lt;/li&gt;
&lt;li&gt;✅ Session management via cookies&lt;/li&gt;
&lt;li&gt;✅ Secure password storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All you need is your SMTP credentials added to your Docker Compose file. I use Resend for email delivery — it's straightforward to set up and works reliably. Everything else is handled by Directus out of the box.&lt;/p&gt;




&lt;p&gt;This is the auth setup I use across all my TanStack Start projects. Combined with Directus roles and permissions, it covers everything a production app needs without adding a separate auth service to your stack.&lt;/p&gt;

&lt;p&gt;Have questions about the setup? Drop a comment below.&lt;/p&gt;

</description>
      <category>directus</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>react</category>
    </item>
    <item>
      <title>Why I Switched to a VPS with Coolify for Hosting My Full Stack Apps</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:22:24 +0000</pubDate>
      <link>https://dev.to/wadethomastt/why-i-switched-to-a-vps-with-coolify-for-hosting-my-full-stack-apps-3hce</link>
      <guid>https://dev.to/wadethomastt/why-i-switched-to-a-vps-with-coolify-for-hosting-my-full-stack-apps-3hce</guid>
      <description>&lt;p&gt;There are plenty of great hosting options out there — Netlify, Vercel, Railway and more. I've tried a few of them and they all have their place. But after going through a few different setups, I landed on a VPS managed with Coolify and it's been the best experience for my workflow.&lt;/p&gt;

&lt;p&gt;Here's how I got there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I Started
&lt;/h2&gt;

&lt;p&gt;I started with Netlify. It's beginner friendly, deployment is simple and it works well for frontend projects. I then tried Vercel and that was also a solid experience — great DX, fast deploys, good defaults.&lt;/p&gt;

&lt;p&gt;Both are genuinely good platforms. But as my projects grew more complex — running a frontend, a Directus instance, a database — the platform costs and constraints started to feel limiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Raw VPS Experiment
&lt;/h2&gt;

&lt;p&gt;I decided to try self-hosting on an Ubuntu VPS with Nginx as my web server and reverse proxy. Manually configuring everything — SSL, domains, process management, deployments. It was a solid learning experience and I'd recommend it to anyone who wants to understand how hosting actually works under the hood.&lt;/p&gt;

&lt;p&gt;But it wasn't the ideal long-term setup. Too much manual work, too much that could go wrong quietly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering Coolify
&lt;/h2&gt;

&lt;p&gt;Coolify changed things. It's a self-hosted platform that sits on top of your VPS and gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic deployments&lt;/strong&gt; — push code and your frontend deploys automatically via Git integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose support&lt;/strong&gt; — I manage my Directus instance and PostgreSQL database through Coolify using Docker Compose, which keeps everything clean and reproducible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxy handled for you&lt;/strong&gt; — no more manually writing Nginx configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL out of the box&lt;/strong&gt; — certificates are handled automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hidden fees&lt;/strong&gt; — you pay for the VPS, that's it. Coolify itself is free to self-host&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Full Control, Your Way
&lt;/h2&gt;

&lt;p&gt;With a VPS you decide how it's configured. You're not subject to platform pricing changes, compute limits, or vendor lock-in. Everything runs on infrastructure you control.&lt;/p&gt;

&lt;p&gt;The tradeoff is responsibility — you manage the server, handle updates, and own any issues that come up. It's not the right fit for everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is It Worth It?
&lt;/h2&gt;

&lt;p&gt;If you're comfortable with a bit of server management and want a flexible, cost-effective setup for running full stack apps, a VPS with Coolify is hard to beat. It's the sweet spot between the ease of a managed platform and the control of raw infrastructure.&lt;/p&gt;

&lt;p&gt;For my stack — TanStack Start on the frontend, Directus as the backend, PostgreSQL as the database — it handles everything cleanly in one place.&lt;/p&gt;




&lt;p&gt;Have questions about the setup or want to know if it's a good fit for your project? Drop a comment below.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>webdev</category>
      <category>selfhosted</category>
      <category>coolify</category>
    </item>
    <item>
      <title>Why I Use Directus as My Backend — Flexible, Self-Hosted and Production Ready</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:15:39 +0000</pubDate>
      <link>https://dev.to/wadethomastt/why-i-use-directus-as-my-backend-flexible-free-and-production-ready-g6g</link>
      <guid>https://dev.to/wadethomastt/why-i-use-directus-as-my-backend-flexible-free-and-production-ready-g6g</guid>
      <description>&lt;p&gt;If you're looking for a backend that's flexible, affordable and genuinely enjoyable to work with, Directus is worth a serious look.&lt;/p&gt;

&lt;p&gt;Here's why it's become my default choice for full stack projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free to Self-Host (With a Sensible Threshold)
&lt;/h2&gt;

&lt;p&gt;Directus uses a BSL 1.1 license. If your organization has less than $5M in total annual income — revenue and funding combined — you can self-host it for free across all your projects, including production and commercial ones. No feature paywalls, no artificial limits.&lt;/p&gt;

&lt;p&gt;Once you're past that threshold, a commercial license is required. But for the vast majority of indie developers, freelancers, startups and small teams, it's completely free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works with Any SQL Database
&lt;/h2&gt;

&lt;p&gt;Directus sits on top of your existing database rather than replacing it. It supports PostgreSQL, MySQL, SQLite, MariaDB and more. My preference is PostgreSQL — pair them together and you get a rock solid data layer with Directus handling the API, auth, and admin UI on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flexible Data Structures
&lt;/h2&gt;

&lt;p&gt;Whether you're building a simple blog, an e-commerce store, or a complex multi-tenant app, Directus adapts to your data model. You define your collections and fields, and Directus generates a full REST and GraphQL API automatically. No rigid schema to fight against.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-In Features That Scale With You
&lt;/h2&gt;

&lt;p&gt;Starting small? Directus is simple enough to get up and running fast. Ready to grow? It has you covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; — built in, with session and token support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles and Permissions&lt;/strong&gt; — granular access control per collection and field&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation&lt;/strong&gt; — flows and webhooks for backend logic without extra services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; — a full admin UI out of the box for managing your data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File Management&lt;/strong&gt; — upload, transform and serve assets directly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Actively Maintained
&lt;/h2&gt;

&lt;p&gt;Directus is backed by a strong team of developers and an active open source community. It's regularly updated, well documented, and not going anywhere. That matters when you're building something you plan to maintain long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who It's For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Startups&lt;/strong&gt; — get a full backend running quickly without over-engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freelancers&lt;/strong&gt; — one backend you can reuse across different client projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Growing products&lt;/strong&gt; — the feature set scales as your requirements do&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developers who want control&lt;/strong&gt; — self-host it, own your data, no vendor lock-in&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I use Directus with TanStack Start on the frontend and the Directus SDK for all data fetching. If you're exploring it for your next project and want to see how the integration works in practice, I've covered the SDK setup and data fetching patterns in previous posts on my profile.&lt;/p&gt;

&lt;p&gt;Have a project in mind and want to know if Directus is the right fit? Drop a comment and I'm happy to help.&lt;/p&gt;

</description>
      <category>direct</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>backend</category>
    </item>
    <item>
      <title>TanStack Start: Metadata, Data Loading and Loading Skeletons in One Route File</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:11:22 +0000</pubDate>
      <link>https://dev.to/wadethomastt/tanstack-start-metadata-data-loading-and-loading-skeletons-in-one-route-file-1cpi</link>
      <guid>https://dev.to/wadethomastt/tanstack-start-metadata-data-loading-and-loading-skeletons-in-one-route-file-1cpi</guid>
      <description>&lt;p&gt;TanStack Start has become my go-to for full stack React apps. One of the things I enjoy most about it is how cleanly it handles three things that are usually spread across multiple files and libraries — metadata, data fetching, and loading UI — all colocated in a single route.&lt;/p&gt;

&lt;p&gt;Here's how I use &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;loader&lt;/code&gt;, and &lt;code&gt;pendingComponent&lt;/code&gt; together.&lt;/p&gt;

&lt;h2&gt;
  
  
  head — Per-Route Metadata
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;head&lt;/code&gt; lets you define document-level metadata per route. Page title, meta description, open graph tags — all scoped to that route without a global workaround or a third party head manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;meta&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All Articles — Big Cats&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every story, every species.&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;Each route owns its own SEO info. No context providers, no helmet libraries, no prop drilling.&lt;/p&gt;

&lt;h2&gt;
  
  
  loader — Data Fetching Before Render
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;loader&lt;/code&gt; is where you fetch everything your route needs. It runs on the server or client depending on your SSR config, and the data it returns is available in your component via &lt;code&gt;useLoaderData&lt;/code&gt;. Parallel fetches with &lt;code&gt;Promise.all&lt;/code&gt; are the standard pattern here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;loader&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getCategories&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&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;Both fetches run in parallel — no waterfall, no useEffect, no loading state management in the component. The data is just there when the component renders.&lt;/p&gt;

&lt;h2&gt;
  
  
  pendingComponent — Skeleton While the Loader Resolves
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pendingComponent&lt;/code&gt; renders while the loader is still resolving. It's the right place for skeletons or placeholder UI. The &lt;code&gt;pendingMs&lt;/code&gt; and &lt;code&gt;pendingMinMs&lt;/code&gt; options give you timing control — useful for avoiding a skeleton flash on fast connections while still showing it on slow ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;pendingComponent&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border-b border-white/10 px-6 py-16 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font-bold tracking-tight md:text-6xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        All Articles
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-white/50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Every story, every species.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto max-w-6xl px-6 py-16"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostGridSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout and headings render immediately while &lt;code&gt;PostGridSkeleton&lt;/code&gt; holds the place for the actual content. The page feels instant even on slower connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  All Together in One Route
&lt;/h2&gt;

&lt;p&gt;What I appreciate most is that all three live in the same route file. The metadata, the data fetching, and the loading UI are colocated — no jumping between files to understand what a route does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;head&lt;/span&gt;&lt;span class="p"&gt;:&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="na"&gt;meta&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All Articles — Big Cats&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every story, every species.&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="na"&gt;loader&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nf"&gt;getCategories&lt;/span&gt;&lt;span class="p"&gt;()&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="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;categories&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;pendingComponent&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border-b border-white/10 px-6 py-16 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font-bold tracking-tight md:text-6xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          All Articles
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mt-3 text-white/50"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Every story, every species.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto max-w-6xl px-6 py-16"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostGridSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;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;If you're evaluating TanStack Start for your next full stack project, this colocation pattern alone is worth the look. Everything your route needs — SEO, data, loading UI — in one place.&lt;/p&gt;




&lt;p&gt;I pair this with Directus as my CMS and Tailwind CSS v4 for styling. The &lt;code&gt;getPosts()&lt;/code&gt; and &lt;code&gt;getCategories()&lt;/code&gt; calls come from a single &lt;code&gt;directus.ts&lt;/code&gt; file using the Directus SDK — I covered that setup in a previous post if you want to see how that layer is structured.&lt;/p&gt;

</description>
      <category>tanstack</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Rendering Rich Text Safely in TanStack Start with Directus, DOMPurify &amp; Tailwind Typography</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 04:04:31 +0000</pubDate>
      <link>https://dev.to/wadethomastt/rendering-rich-text-safely-in-tanstack-start-with-directus-dompurify-tailwind-typography-16b6</link>
      <guid>https://dev.to/wadethomastt/rendering-rich-text-safely-in-tanstack-start-with-directus-dompurify-tailwind-typography-16b6</guid>
      <description>&lt;p&gt;If you're using Directus as a headless CMS, the built-in WYSIWYG editor outputs raw HTML. The challenge is rendering it in React safely, with proper styling, and without reaching for &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how to solve it with three packages working together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DOMPurify&lt;/strong&gt; — sanitizes HTML, stripping XSS vectors before they touch the DOM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;html-react-parser&lt;/strong&gt; — converts sanitized HTML into React elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS v4 Typography&lt;/strong&gt; — styles everything automatically with the &lt;code&gt;prose&lt;/code&gt; class&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installing the Packages
&lt;/h2&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;dompurify html-react-parser
npm &lt;span class="nb"&gt;install&lt;/span&gt; @tailwindcss/typography
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1 — Sanitize the HTML
&lt;/h2&gt;

&lt;p&gt;Create a reusable utility function that runs DOMPurify on any HTML string coming from Directus. The &lt;code&gt;typeof window === 'undefined'&lt;/code&gt; guard handles SSR — DOMPurify is browser-only.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitizeHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;h2&gt;
  
  
  Step 2 — Parse and Render
&lt;/h2&gt;

&lt;p&gt;Pass the sanitized HTML through &lt;code&gt;html-react-parser&lt;/code&gt; to convert it into React elements. Wrap it in a &lt;code&gt;prose&lt;/code&gt; div and Tailwind Typography handles all the styling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;parse&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;html-react-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;sanitizeHtml&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;@/utils/sanitizeHtml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="na"&gt;prose&lt;/span&gt; &lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;prose-invert&lt;/span&gt; &lt;span class="na"&gt;prose-ul&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;text-foreground&lt;/span&gt;
  &lt;span class="na"&gt;prose-li&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;marker&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;text-foreground&lt;/span&gt; &lt;span class="na"&gt;prose-strong&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="na"&gt;text-foreground&lt;/span&gt; &lt;span class="na"&gt;max-w-none&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sanitizeHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ XSS safe — DOMPurify strips malicious scripts before rendering&lt;/li&gt;
&lt;li&gt;✅ No &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; — html-react-parser converts to React elements directly&lt;/li&gt;
&lt;li&gt;✅ Dark mode ready — &lt;code&gt;prose-invert&lt;/code&gt; handles the color flip automatically&lt;/li&gt;
&lt;li&gt;✅ Design system aware — &lt;code&gt;prose-strong:text-foreground&lt;/code&gt; and &lt;code&gt;prose-ul:text-foreground&lt;/code&gt; pull from your CSS variables so text always matches your theme&lt;/li&gt;
&lt;li&gt;✅ Headings, lists, bold, spacing — all styled automatically by Tailwind Typography&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Not Just Use dangerouslySetInnerHTML?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; renders raw HTML with no sanitization. If your CMS content ever contains injected scripts — whether from a compromised editor, a bad import, or a malicious user — it executes directly in the browser. DOMPurify + html-react-parser gives you the same rendered output with none of the risk.&lt;/p&gt;




&lt;p&gt;This pattern works with any headless CMS that outputs HTML from a rich text editor — Directus, Payload, Strapi, or Contentful. One utility function and a prose wrapper is all it takes.&lt;/p&gt;

</description>
      <category>directus</category>
      <category>webdev</category>
      <category>typescript</category>
      <category>react</category>
    </item>
    <item>
      <title>Connecting TanStack Start to Directus with the SDK — Type-Safe Data Fetching in One File</title>
      <dc:creator>Wade Thomas</dc:creator>
      <pubDate>Mon, 30 Mar 2026 03:45:05 +0000</pubDate>
      <link>https://dev.to/wadethomastt/connecting-tanstack-start-to-directus-with-the-sdk-type-safe-data-fetching-in-one-file-1e0c</link>
      <guid>https://dev.to/wadethomastt/connecting-tanstack-start-to-directus-with-the-sdk-type-safe-data-fetching-in-one-file-1e0c</guid>
      <description>&lt;p&gt;If you're using Directus as your headless CMS and TanStack Start for your frontend, you don't need to write manual fetch calls or build your own auth headers. The Directus SDK handles all of it cleanly.&lt;/p&gt;

&lt;p&gt;Here's how I structure a single &lt;code&gt;directus.ts&lt;/code&gt; file that covers authentication, typed data fetching, filtering, CRUD operations and file uploads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the SDK
&lt;/h2&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; @directus/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up the Client
&lt;/h2&gt;

&lt;p&gt;Create the client once and export it. Passing your schema type as a generic is what unlocks end-to-end type safety across every request.&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Navigation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CartItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Order&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;@/types&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;authentication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updateItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;deleteItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;readMe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updateMe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;deleteUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;uploadFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;registerUser&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;registerUserDirectus&lt;/span&gt;&lt;span class="p"&gt;,&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;@directus/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directusUrl&lt;/span&gt; &lt;span class="o"&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;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_DIRECTUS_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-directus-url.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;directus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDirectus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directusUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;session&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="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&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="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;authentication('session')&lt;/code&gt; with &lt;code&gt;credentials: 'include'&lt;/code&gt; means cookies are handled automatically — no manual token management needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching Data with Full Type Safety
&lt;/h2&gt;

&lt;p&gt;Each collection gets its own exported async function with a typed return value.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&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;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Product&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;h2&gt;
  
  
  Filtering is First-Class
&lt;/h2&gt;

&lt;p&gt;The SDK's filter syntax maps directly to Directus's query engine — no raw query strings, no URL building.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProductsByCategory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&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;directus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;readItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&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="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_eq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;category&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Product&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;h2&gt;
  
  
  What Else is Covered
&lt;/h2&gt;

&lt;p&gt;The same client and pattern covers the full CRUD surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;readItems&lt;/code&gt; — fetch collections&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;createItem&lt;/code&gt; — insert records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updateItem&lt;/code&gt; — update records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;deleteItem&lt;/code&gt; — delete records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uploadFiles&lt;/code&gt; — handle file uploads&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;readMe&lt;/code&gt; / &lt;code&gt;updateMe&lt;/code&gt; / &lt;code&gt;deleteUser&lt;/code&gt; — user profile management&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;registerUser&lt;/code&gt; — user registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All importable directly from @directus/sdk — the SDK provides typed functions and you bring your own collection types for end-to-end type safety.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it in a TanStack Start Loader
&lt;/h2&gt;

&lt;p&gt;TanStack Start's file-based routing and loader pattern pairs perfectly with this setup. Data is fetched server-side and ready before the component renders.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getProducts&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;No boilerplate, no custom wrappers, no type assertions. One file, full coverage.&lt;/p&gt;




&lt;p&gt;If you're evaluating Directus as a headless CMS for a TanStack Start project this setup gets you up and running quickly with a clean, maintainable data layer from day one.&lt;/p&gt;

</description>
      <category>directus</category>
      <category>tanstack</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
