I’ve been thinking about how we build web applications lately. There’s a constant push for more speed, better reliability, and fewer bugs. I found myself asking a simple question: what if we could make the entire stack, from the database to the browser, understand exactly what data it’s working with? This led me to combine two specific tools: the Remix framework and Drizzle ORM. The result is a way to build applications where the types flow seamlessly from your SQL tables to your React components.
Remix handles the full application lifecycle. It manages routing, server-side rendering, and data loading. Drizzle is a different kind of database tool. It’s not a heavy, complex system. It’s a lightweight library that lets you write SQL queries with TypeScript. You define your database structure once, and Drizzle gives you a fully typed client to work with it. When you put them together, you close the loop on type safety.
Let’s start with the foundation: your database schema. With Drizzle, you define your tables using a declarative syntax in TypeScript. This isn’t just configuration; it’s the single source of truth for your data’s shape.
// app/db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
publishedAt: timestamp('published_at'),
});
This code does more than describe tables. It creates TypeScript interfaces. The users table definition also creates a User type. Every query you write will know that a user has an id (number), an email (string), and so on. Have you ever had a runtime error because you misspelled a database column name? That stops here.
The next step is integrating this into Remix. Remix has two key server-side concepts: Loaders and Actions. Loaders fetch data for a route. Actions handle form submissions and data mutations. This is where Drizzle comes in.
First, set up a database client. You typically do this in a utility file so you can import it anywhere.
// app/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
Now, inside a Remix route, you can use this client in a loader. The beauty is in the type inference. Look at this example for a blog post page.
// app/routes/posts.$id.tsx
import { json, LoaderFunctionArgs } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { db } from '~/db';
import { posts, users } from '~/db/schema';
import { eq } from 'drizzle-orm';
export async function loader({ params }: LoaderFunctionArgs) {
const postId = Number(params.id);
const result = await db
.select({
post: posts,
author: users,
})
.from(posts)
.where(eq(posts.id, postId))
.leftJoin(users, eq(posts.authorId, users.id))
.limit(1);
if (result.length === 0) {
throw new Response('Not Found', { status: 404 });
}
const { post, author } = result[0];
return json({ post, author });
}
export default function PostPage() {
const { post, author } = useLoaderData<typeof loader>();
// `post` and `author` are fully typed here.
return (
<article>
<h1>{post.title}</h1>
<p>By {author?.name}</p>
<div>{post.content}</div>
</article>
);
}
See what happened? The loader fetches the data. The useLoaderData<typeof loader>() hook in the component gets the exact type returned by the loader. The post.title property is known to be a string. If you tried to access post.titl by mistake, TypeScript would catch it immediately. The database query, the server response, and the UI component are all linked by one chain of types.
What about writing data? Actions handle that. Imagine a form to create a new comment. The process remains type-safe from the form validation all the way to the database insert.
// app/routes/posts.$id.comment.tsx
import { ActionFunctionArgs, json } from '@remix-run/node';
import { db } from '~/db';
import { comments } from '~/db/schema';
import { z } from 'zod';
// Define validation schema
const CommentSchema = z.object({
content: z.string().min(1),
authorName: z.string().optional(),
});
export async function action({ request, params }: ActionFunctionArgs) {
const postId = Number(params.id);
const formData = await request.formData();
const rawData = Object.fromEntries(formData);
// Validate form data
const result = CommentSchema.safeParse(rawData);
if (!result.success) {
return json({ errors: result.error.flatten() }, { status: 400 });
}
const { content, authorName } = result.data;
// Insert into DB with full type-checking
const [newComment] = await db
.insert(comments)
.values({
postId: postId,
content: content,
authorName: authorName || 'Anonymous',
})
.returning(); // Returns the inserted row
return json({ success: true, comment: newComment });
}
This pattern is powerful. The zod library validates the raw form data. Once validated, we know result.data matches a structure Drizzle expects. The db.insert().values() call is checked against the comments schema. If you added a new required field to the comments table but forgot to update this action, TypeScript would tell you. It prevents the database itself from throwing an error at runtime.
Performance is a natural outcome of this setup. Remix encourages you to fetch data on the server, right before you render. There’s no need for a separate client-side fetch that waits for JavaScript to load. Drizzle supports this model perfectly. You can write efficient, specific queries. If you need the raw power of SQL for a complex report, you can drop down to a raw query and still get typed results back.
const salesData = await db.execute<{
month: string;
total: number;
}>(sql`SELECT TO_CHAR(created_at, 'YYYY-MM') as month,
SUM(amount) as total
FROM orders
GROUP BY month`);
The combination changes the development experience. You spend less time debugging “cannot read property X of undefined” errors. You spend more time building features. Refactoring becomes less scary because the compiler guides you. When you change a column from a string to a number, every part of your app that uses it will light up with type errors until you fix it. This is a good thing. It turns runtime problems into compile-time hints.
Building software is about managing complexity. By integrating Remix and Drizzle, you create a clear, type-safe pipeline for data. It starts in your database, moves through your server logic, and ends up rendered on the screen. Each step understands the data’s contract. This approach reduces bugs, improves developer confidence, and lets you ship features faster. Isn’t that what we’re all trying to do?
Give this integration a try on your next project. Start with a simple schema, connect a loader, and feel the confidence that comes from full-stack type safety. If you’ve built something similar or have questions about the approach, I’d love to hear about it in the comments below. If this breakdown was helpful, please consider sharing it with other developers who might be wrestling with these same challenges.
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva