Vafast + Drizzle: A Lightweight, Efficient Full-Stack Type-Safe Stack

Drizzle ORM is a TypeScript ORM that has drawn a lot of attention in recent years, known for being lightweight, fast and SQL-like.
Unlike Prisma, Drizzle has no code generation step; types are inferred directly from the schema definition, which makes it a perfect fit for Vafast's type system.
Why Drizzle
Lightweight: Drizzle's package is tiny, with no complex runtime dependencies.
SQL-like syntax: if you know SQL, Drizzle's API will feel very natural.
Zero code generation: no extra generate step; types are available immediately.
Edge-runtime friendly: Drizzle runs on edge environments such as Cloudflare Workers and Vercel Edge.
Quick Start
First, create a Vafast project:
npx create-vafast-app vafast-drizzle
cd vafast-drizzleInstall the Drizzle dependencies:
npm add drizzle-orm better-sqlite3
npm add -D drizzle-kit @types/better-sqlite3We use SQLite for this demo; Drizzle also supports PostgreSQL, MySQL and other databases.
Defining the Schema
Create src/db/schema.ts:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
username: text('username').notNull().unique(),
email: text('email').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('author_id')
.notNull()
.references(() => users.id),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date())
})Create src/db/index.ts:
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import * as schema from './schema'
const sqlite = new Database('sqlite.db')
export const db = drizzle(sqlite, { schema })Configuring Drizzle Kit
Create drizzle.config.ts:
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
dbCredentials: {
url: 'sqlite.db'
}
})Generate and push the database:
npx drizzle-kit pushBuilding the API
Now let's build a CRUD API with Vafast:
import { Server, defineRoute, defineRoutes, serve, Type, err } from 'vafast'
import { db } from './db'
import { users, posts } from './db/schema'
import { eq } from 'drizzle-orm'
// define the schemas
const CreateUserBody = Type.Object({
username: Type.String({ minLength: 3 }),
email: Type.String({ format: 'email' })
})
const CreatePostBody = Type.Object({
title: Type.String({ minLength: 1 }),
content: Type.String(),
authorId: Type.Number()
})
const routes = defineRoutes([
// user routes
defineRoute({
method: 'GET',
path: '/users',
handler: async () => {
return await db.select().from(users)
}
}),
defineRoute({
method: 'GET',
path: '/users/:id',
schema: { params: Type.Object({ id: Type.String() }) },
handler: async ({ params }) => {
const user = await db
.select()
.from(users)
.where(eq(users.id, Number(params.id)))
.get()
if (!user) {
throw err.notFound('User not found')
}
return user
}
}),
defineRoute({
method: 'POST',
path: '/users',
schema: { body: CreateUserBody },
handler: async ({ body }) => {
const result = await db.insert(users).values(body).returning()
return result[0]
}
}),
// post routes
defineRoute({
method: 'GET',
path: '/posts',
handler: async () => {
return await db.select().from(posts)
}
}),
defineRoute({
method: 'POST',
path: '/posts',
schema: { body: CreatePostBody },
handler: async ({ body }) => {
const result = await db.insert(posts).values(body).returning()
return result[0]
}
}),
// get all posts by a user
defineRoute({
method: 'GET',
path: '/users/:id/posts',
schema: { params: Type.Object({ id: Type.String() }) },
handler: async ({ params }) => {
return await db
.select()
.from(posts)
.where(eq(posts.authorId, Number(params.id)))
}
})
])
const server = new Server(routes)
serve({ fetch: server.fetch, port: 3000 }, () => {
console.log('Server running at http://localhost:3000')
})Relational Queries
Drizzle supports powerful relational queries. Let's add an endpoint that returns a post with its author:
import { db } from './db'
import { users, posts } from './db/schema'
import { eq } from 'drizzle-orm'
// get a post with its author
defineRoute({
method: 'GET',
path: '/posts/:id/detail',
schema: { params: Type.Object({ id: Type.String() }) },
handler: async ({ params }) => {
const result = await db
.select({
post: posts,
author: {
id: users.id,
username: users.username
}
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.id, Number(params.id)))
.get()
if (!result) {
throw err.notFound('Post not found')
}
return {
...result.post,
author: result.author
}
}
})Transactions
Drizzle supports transactions to keep data consistent:
const TransferBody = Type.Object({
fromUserId: Type.Number(),
toUserId: Type.Number(),
postId: Type.Number()
})
defineRoute({
method: 'POST',
path: '/posts/transfer',
schema: { body: TransferBody },
handler: async ({ body }) => {
return await db.transaction(async (tx) => {
// check that the post belongs to the original author
const post = await tx
.select()
.from(posts)
.where(eq(posts.id, body.postId))
.get()
if (!post || post.authorId !== body.fromUserId) {
throw err.badRequest('Not allowed to transfer this post')
}
// update the post's author
const result = await tx
.update(posts)
.set({ authorId: body.toUserId })
.where(eq(posts.id, body.postId))
.returning()
return result[0]
})
}
})Edge Deployment
A big advantage of Drizzle is edge runtime support. Using D1 (Cloudflare's SQLite):
// using it in Cloudflare Workers
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './db/schema'
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB, { schema })
const server = new Server(routes)
return server.fetch(request)
}
}Why Vafast + Drizzle
Type consistency: Drizzle's schema types work perfectly with Vafast's Type schemas, giving full type safety from the database to the API.
No generate step: no need to run prisma generate; types are available as soon as you change the schema.
Edge friendly: both support a range of edge runtimes, so you can deploy easily to Cloudflare Workers, Vercel Edge and more.
High performance: Vafast's fast routing + Drizzle's lightweight queries deliver top-notch performance.
Summary
Drizzle ORM and Vafast together give TypeScript developers a lightweight, efficient and type-safe full-stack solution.
If you want a smaller bundle, faster startup and a development experience closer to SQL, Drizzle + Vafast is well worth trying.
See more: