From Good to Great: Boosting Dev Experience with Drizzle ORM & Directus
Discover the synergy of Directus and Drizzle ORM. Elevate your TypeScript experience, streamline database operations, and supercharge your web development workflow in just a few steps.
Directus is packed with features we all love, but there's been a missing piece: TypeScript support when querying data. That's where Drizzle ORM comes in. In this article, we'll explore how using Drizzle ORM with Directus can make our development process even better and solve the TypeScript challenge.
Exploring TypeScript Support in the Directus JavaScript SDK
The Directus JavaScript SDK boasts a notable commitment to TypeScript, highlighting its dedication to offering developers a "TypeScript first" experience. This pledge is evident when we see the enticing feature tag:
TypeScript first: The SDK provides a robust and type-safe development experience
It's clear that Directus aims to ensure developers can work seamlessly with TypeScript.
However, when delving further into the SDK's documentation, we encounter:
If using TypeScript, you need to provide a
Schema
when creating a Directus client to make use of type hinting and completion. This schema contains definitions for each collection and provides you with type hints (on input) and completion (on output).
This means developers are tasked with implementing the schema themselves.
The Challenge of Manual Schema Implementation
While Directus offers a systematic approach to integrating TypeScript, there's a clear challenge: manual schema implementation.
For developers who thrive on efficiency, creating and updating the schema by hand, especially for larger projects with numerous collections, can become a time-consuming task.
Let's consider the process: for each collection in Directus, a corresponding TypeScript interface has to be crafted, ensuring that it matches the data structure in Directus precisely. This not only demands meticulous attention to detail but also a significant investment of time, especially when modifications are made to the Directus collections.
It's not that manual implementations are inherently problematic, but in an era where automation and seamless workflows are prized, there's certainly room for improvement. And while Directus's "TypeScript first" approach is commendable, the manual schema aspect does leave a few developers wishing for a more streamlined experience.
So, where do we go from here? Instead of resigning ourselves to the manual grind, let's explore some alternatives that can make our TypeScript journey with Directus smoother and more efficient.
Harnessing Automation for Schema Generation
Navigating the manual intricacies of schema implementation can be cumbersome, but the developer landscape is vast, with many tools and methods at our disposal. The ideal approach would be to strike a balance between automation and precision, ensuring that we maintain the integrity of our data while speeding up the development process. In our quest for a more efficient TypeScript journey with Directus, let's delve into a promising option that can provide the solution we're after.
Option 1: ChatGPT-Assisted Schema Creation
Overview:
Leverage the capabilities of ChatGPT to automate the generation of Zod schemas or TypeScript interfaces from given table definitions or data schemas.
Advantages:
- Efficiency Boost: Simply copy and paste your table definition or data schema into ChatGPT and let it handle the heavy lifting.
- Error Minimization: With a well-phrased prompt and a touch of manual verification, the chances of errors are significantly reduced.
Considerations:
- Naming Consistency: There might be occasional inconsistencies in naming, so it's essential to double-check the generated schema.
- Updating Schemas: Remember, if there's a change in your Directus collections, you'll need to manually copy the updated schema into ChatGPT for a fresh generation.
Option 2: directus-typescript-gen
Overview:
The directus-typescript-gen
tool aims to simplify the TypeScript integration process with Directus. By dynamically extracting typings from a live Directus server, it generates the necessary TypeScript definition files for the Directus TypeScript SDK. This ensures that developers not only get accurate definitions but also enjoy features like type-checking, autocompletion, and other TypeScript advantages.
There's a great blogpost on izoukhai.com that goes through this appraoch in more detailed steps.
Usage:
Initiate the generator on a running Directus server to get the TypeScript definitions:
Advantages:
- Automation: Bypass the manual process by dynamically extracting typings directly from a live server.
- Enhanced TypeScript Experience: Enjoy the perks of type-checking, autocompletion, and other TypeScript features seamlessly.
- User-Friendly: Just run a simple command to get the required TypeScript definitions, making it relatively straightforward even for those unfamiliar with Directus or TypeScript.
Considerations:
- Update Status: The tool's last update was two years ago, raising questions about its current compatibility and support for newer versions of Directus.
- Consistency: While it worked effectively for a past project, its current reliability may need validation, given the time since its last update.
Option 3: Drizzle ORM
Overview:
Drizzle ORM stands out as a robust TypeScript Object Relational Mapping (ORM) solution tailored for developers seeking both performance and longevity. Beyond merely mapping database entries to TypeScript objects, Drizzle offers a many features and benefits that makes it an excellent fit for those wanting to supercharge their Directus TypeScript experience.
Key Features:
- Lightweight & Edge Ready: Drizzle is designed to be nimble, ensuring top-notch performance across various platforms.
- Hassle-Free SQL Migrations: Say goodbye to the intricacies of SQL migrations; Drizzle simplifies the process.
- No Code Generation: Eliminate the need for redundant code generation processes.
- Zero Dependencies: A cleaner codebase without additional dependencies.
- Rich SQL Dialects: Catering to a variety of SQL dialects to enhance database interactions.
- Broad Runtime Support: From Cloudflare Workers and Supabase functions to Vercel functions and Browser integrations, Drizzle has extensive compatibility.
- Diverse Database Connectivity: Whether it's PostgreSQL, MySQL, or SQLite, Drizzle can seamlessly connect to a wide array of databases.
If you're interested in a more detailed introduction of Drizzle, check out this article.
Unveiling the Power of Drizzle ORM: Key Features that Skyrocketed My Productivity
Want to supercharge your dev productivity? Get a glimpse into how Drizzle ORM, with its well-structured docs and powerful features, could be a game-changer for your projects.
Advantages:
- Comprehensive ORM Solution: Drizzle offers an all-in-one solution for those aiming to intertwine TypeScript and databases effectively.
- Future-Proofing: Designed with longevity in mind, adopting Drizzle is a long-term investment in efficient database management.
- Ecosystem Integration: With support for major serverful and serverless runtimes and various databases, Drizzle ensures a broad spectrum of compatibility.
Considerations:
- Learning Curve: As with any ORM, there might be an initial learning curve, especially for those new to Drizzle's specifics.
Why I Chose Drizzle ORM Over Others
Making a decision among a slew of good options is never straightforward. However, certain standout features and benefits can tip the balance. For my project, Drizzle ORM emerged as the frontrunner, and here's why:
- Instant Schema Generation: The true power of an ORM or a development tool often lies in the time it saves. Drizzle ORM’s
drizzle-kit introspect:{dialect}
command is a testament to this. With just a quick command, I was able to pull the Data Definition Language (DDL) from my existing database and generate the requiredschema.ts
file in a snap. This meant not only TypeScript interfaces but also Zod models were ready in mere seconds. - Direct Database Access: One of the most significant advantages Drizzle ORM offers is its independence from the Directus server. Instead of routing my queries through Directus, I can communicate directly with the database. This direct approach makes it lightweight and perfect for edge deployments, ensuring speed and efficiency.
- Intuitive Query Syntax: We all have our preferences when it comes to coding syntax. With the traditional JavaScript SDK for Directus, I often found myself navigating through nested and sometimes convoluted structures. Drizzle ORM, on the other hand, empowers me with familiar SQL dialects, all while maintaining top-notch type safety. This combo not only feels natural but also streamlines the development process, making it more enjoyable and efficient.
For context, let's first look at an implementation using the Directus SDK v11. Shortly, we'll compare it to a in my opionen more streamlined version.
Implementing Drizzle ORM with Directus Collections
Successfully marrying Drizzle ORM with Directus Collections can amplify your development experience manifold. This combination lets you leverage the amazing interface of Directus while tapping into the power-packed features of Drizzle ORM. Here's a step-by-step guide to getting started.
Step 1: Setting Up Directus Models
Your Directus models, or Collections as they're often referred to, serve as the blueprint for your content and dictate how data should be structured within your Directus instance. I set up these models:
And here are the most important models in more detail:
Step 2: Setting Up Drizzle ORM Database Connection
Connecting Drizzle ORM to your database is a pivotal step in our journey. This connection will be the bridge that facilitates all interactions between your Directus models and the underlying database, empowering you with direct, type-safe querying capabilities.
Creating the Configuration File
Firstly, let's set up a configuration file for Drizzle ORM, which I've named directus-drizzle.config.ts
. You can place this file within your project's structure, I placed mine in the frontend package:
📂 path
: packages/frontend/directus-drizzle.config.ts
In this configuration:
- We're using the
dotenv
package to load environment variables from.env.local
. - The connection string is sourced from an environment variable named
DIRECTUS_DIRECT_URL
. - The driver specified here is for a PostgreSQL database (
pg
). Ensure this matches your database type.
For a deeper dive into configuration options and understanding the full capabilities of Drizzle ORM configurations, I highly recommend checking the official Drizzle ORM documentation, especially the section detailing configurations: Drizzle ORM Configuration Documentation.
This setup ensures that Drizzle ORM is finely tuned to interact with your database, respecting the structures you've defined in Directus and facilitating efficient, type-safe operations.
Step 3: Creating the Schema
Generating and maintaining a schema for your database is crucial. When working with Directus and Drizzle, this process becomes streamlined, allowing for quick introspection and schema creation from your database. Let's break down how this is accomplished:
1. Package.json Scripts:
Within our package.json
file, we have defined three scripts for schema management:
- introspect-drizzle: Uses the Drizzle introspection command to generate a schema based on the Directus PostgreSQL database.
- fix-directus-schema: Invokes a script to correct minor issues within the generated schema.
- introspect-fix-directus: This combined script first introspects the database to generate a schema and then applies fixes. It's ideal to include this in your build step.
2. Correcting Schema Generation Issues:
While the introspection process is robust, you might encounter minor issues with the generated schema. We address these with the fix-directus-schema.js
script:
Script to fix minor schema generation issues.
This script modifies the generated schema by making certain replacements to correct minor discrepancies.
Step 4: Creating the Drizzle Client
Once the database connection is set up, the next logical step in our journey is creating a client to handle database operations through Drizzle ORM. This client will act as the main interface for all our database-related tasks, leveraging the power of Drizzle ORM while working harmoniously with the Directus configurations we've established.
Setting Up the Client
To do this, I created a file named directus.ts
located in the frontend's src/lib
directory:
📂 path
: packages/frontend/src/lib/directus.ts
Here's a breakdown of what's happening:
- We're utilizing the
@neondatabase/serverless
package to set up a connection pool. This provides a managed set of database connections that can be used for executing queries, ensuring efficient database communication. - The connection string is fetched from the
DIRECTUS_DATABASE_URL
environment variable, which must be set beforehand. - The Drizzle client is then initialized using the connection pool and the schema we defined earlier. This client will be the core tool you use to query and manipulate your data using Drizzle ORM.
By the end of this step, you have a robust Drizzle client ready to go, backed by a sturdy database connection.
Step 5: Fetching Data - A Practical Example
As we progress, understanding how to harness the combined power of Directus and Drizzle ORM to fetch data becomes critical. Let's delve into a concrete example to fetch and display data for a blog post page.
Here's an example that illustrates the process:
What's happening in the code above?
- Fetching Blog Post Data: We start by making an inner join between the
blogposts
andauthors
tables. We're interested in blog posts that match a certainslug
and have a status of'published'
. - Fetching Associated Categories: We then retrieve the categories associated with the blog post. We make use of inner joins to combine data from the
blogpostCategories
andblogpostsBlogpostCategories
tables. - Fetching Content Data: Finally, we fetch content data related to our blog post, leveraging the
blogpostsContent
table. - Rendering: After fetching all the required data, we use JSX to structure and present the data on the page.
This example underscores the simplicity and power that Drizzle ORM brings to the table. By crafting precise queries, we're able to seamlessly integrate our Directus collections with the application, ensuring a dynamic and efficient content delivery system.
In the next section, we'll delve into the "many-2-any" field, focusing on our content. We will explore how we can effectively render these blocks, ensuring our data is showcased in the best possible manner. Stay tuned!
Step 6: Rendering Dynamic Blocks with Directus and Drizzle ORM
A powerful feature of content management systems (CMS) like Directus is the capability to define dynamic content blocks, which can be conditionally rendered based on data and logic. This flexibility ensures that content creators and developers can model and present data in a versatile and modular manner. Let's dive into how you can set up and render these dynamic content blocks using Directus and Drizzle ORM.
1. render-blocks Component
Our first order of business is to define a React component, render-blocks.tsx
, which will be responsible for rendering each block based on its type:
2. Implementing Block Components
Next, we create separate React components for each block type:
block-richtext.tsx
: Displays rich text content, which can encompass formatted text, hyperlinks, and other HTML elements.
block-image.tsx
: This component fetches and displays an image from Directus.
3. Image Helper Function
To streamline the process of fetching image metadata from Directus, we employ a helper function, getDirectusImageUrl
. This function retrieves crucial data such as image URL, dimensions, and title.
With this helper, fetching image data becomes seamless, enabling us to efficiently render images using the directus-image.tsx
component.
4. Directus Image Component
The diretus-image.tsx
component serves to render an image fetched from Directus, using Next.js's Image
component for optimized image delivery:
With the described setup, we've unlocked a robust method to handle and render dynamic content blocks with Directus and Drizzle ORM. This modular approach not only encapsulates logic for each block type but also facilitates future additions or modifications to the block types.
As you expand your project, simply create new block components and integrate them into the render-blocks.tsx
component. This system is scalable, maintainable, and harnesses the full potential of Directus as a CMS alongside Drizzle ORM's capabilities.
Empowering Development: The Synergy of Directus and Drizzle
As we journeyed through the intricate world of Directus and Drizzle, it's evident that when paired together, these tools can truly elevate your TypeScript development process. By streamlining database operations and seamlessly connecting TypeScript objects with database entries, we can craft applications that are both robust and maintainable.
The power of introspection, complemented by precise schema generation and maintenance techniques, allows for a hassle-free and accurate reflection of our database structures in our codebase. And although we might occasionally encounter minor roadblocks, as seen with the schema generation discrepancies, the community-driven nature of these platforms ensures there are solutions readily available.
In essence, Directus and Drizzle together offer a potent combination for developers aiming to harness the benefits of a headless CMS with the predictability and type safety of TypeScript. As you integrate these practices into your workflow, you'll find that they not only boost productivity but also ensure a more consistent and bug-free development experience.
Happy coding!