TL:DR
This article is a continuation of my earlier Turso Article, you can go through that article and the code first, but it's not required for reading or taking over changes from this article if you want to code along.
Improved:
useDatabase()
is now globally available, no need to create it yourself.useDatabase()
is pre-configured through yournuxt. | nitro.config.ts file
.- Enabling the experimental database layer will automagically add a local dev sqlite db at
./.data/db.sqlite3
. - You can use the experimental
Nitro Tasks
to perform scheduled migrations or perform any other Ops on your db. - It is way cleaner compared to the previous approach.
Requirements:
- You need to update your
package.json
to include the following versions: Note: Delete your package-lock file after version update and before re-installing!
"nuxi": "npm:nuxi-nightly@latest",
"nuxt": "npm:nuxt-nightly@latest",
- You need to install the
better-sqlite3
dependency for the local dev database layer. You can find my working experimental version at the nightly branch of previous article.
Going over the changes
In this article I will be going over some amazing new experimental features introduced inside the nightly build of Nuxt and Nitro. Introducing db0, a new lightweight module to connect with your SQL databases natively through Nitro, how awesome is that!
It uses Drizzle ORM internally and offers a bunch more ORM's soon (check the integrations). Since we are already focused on Drizzle, this seemed like a win-win to me.
I'll be taking you step by step on what the database layer adds and what has been improved in terms of setting up your database inside your Nuxt 3 projects.
- Getting started
- Nuxt Configuration
- Usage
- Next up Nitro Tasks: Database Migration Example
- What about my
drizzle.config.ts
and typed SQL schema? - Nuxt / Nitro API Example
- References
1. Getting started
If you are new and tagging along, make sure you have your Turso DB API token at hand.
You'll need them in your .env
file otherwise the database layer will kick off the local dev db.
devDatabase=true
TURSO_DB_URL=
TURSO_DB_AUTH_TOKEN=
Upgrade your project
If you are thinking of upgrading one of your existing Nuxt 3 projects like I did, then do this first.
- **Install
better-sqlite3
,drizzle-kit
anddrizzle-orm
.
pnpm add better-sqlite3 drizzle-kit drizzle-orm
- Adopt the nightly version of Nuxt 3 in your
package.json
file:
// for Nuxt 3
"nuxi": "npm:nuxi-nightly@latest",
"nuxt": "npm:nuxt-nightly@latest",
- Delete your
npm/yarn/pnpm
package-lock file. - Reinstall your project
npm i / yarn i / pnpm i
.
or, clean install the nightly release
- Open terminal and start a
nuxi-nightly
CLI build, use the following CLI command:
npx nuxi-nightly@latest init your-app-name
- **Install
better-sqlite3
,drizzle-kit
anddrizzle-orm
.
pnpm add better-sqlite3 drizzle-kit drizzle-orm
2. Nuxt Configuration
Let's update the nuxt.config.ts
file to enable the new experimental features by adding the nitro.experimental
and nitro.database
object.
- Enable the experimental database and tasks layer inside your
nuxt.config.ts
:
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
devtools: { enabled: true },
nitro: {
experimental: {
tasks: true,
database: true,
},
database: {
devDatabase: {
connector: 'sqlite',
options: { name: 'devDb' }
},
default: {
connector: 'turso',
options: {
url: process.env.TURSO_DB_AUTH_TOKEN,
authToken: process.env.TURSO_DB_AUTH_TOKEN,
}
},
/*
// we're not using this and not in repo
///////////////////////
users: {
connector: 'postgresql',
url: 'postgresql://username:password@hostname:port/database_name'
}
*/
}
},
})
- The new database layer automagically generates a new better-sqlite3 database for us on the go at
./.data/db.sqlite3
. Note: You guessed it right, including thedevDatabase
object automagically overrides all your other databases connectors even the default connector (yes, you can have multiple, at the same time).
devDatabase: {
connector: 'sqlite',
options: { name: 'devDb' }
},
Nitro difference
You configure Nitro the same way as Nuxt and the above nuxt.config
example, however for Nitro, remove the nitro
parent object. Like so:
export default defineNitroConfig({
experimental: {
tasks: true,
database: true,
},
database: {
devDatabase: {
connector: 'sqlite',
options: { name: 'devDb' }
},
default: {
connector: 'turso',
options: {
url: process.env.TURSO_DB_AUTH_TOKEN,
authToken: process.env.TURSO_DB_AUTH_TOKEN,
}
},
/*
// we're not using this and not included in the example repo
///////////////////////
users: {
connector: 'postgresql',
url: 'postgresql://username:password@hostname:port/database_name'
}
*/
}
})
3. Usage
In the previous article we initialized our database inside ./server/utils/db.ts
, lets revisit the code.
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
export const database = createClient({
url: process.env.TURSO_DB_URL!,
authToken: process.env.TURSO_DB_AUTH_TOKEN!,
});
export const orm = drizzle(database);
That's a lot of code... every single time. With the experimental database feature enabled here's what is left of the code.
import { drizzle } from "db0/integrations/drizzle/index";
export const orm = drizzle(useDatabase());
The database layer internalizes the createClient()
and exports useDatabase()
for us.
// Current setup
export const database = createClient({
url: process.env.TURSO_DB_URL!,
authToken: process.env.TURSO_DB_AUTH_TOKEN!,
});
// becomes
export const database = useDatabase()
The module comes with its own built in Drizzle ORM configuration. You can import it with:
import { drizzle } from "db0/integrations/drizzle";
The experimental layer bootstraps a majority of the initial database setup and configuration.
// Current setup
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
export const database = createClient({
url: process.env.TURSO_DB_URL!,
authToken: process.env.TURSO_DB_AUTH_TOKEN!,
});
// becomes
import { drizzle } from "db0/integrations/drizzle";
export const database = useDatabase()
export const orm = drizzle(database)
// or just
import { drizzle } from "db0/integrations/drizzle";
export const orm = drizzle(useDatabase())
/* or just dont?
// ./server/utils/db.ts - not found
// In reality you dont need this util function at all, it's automagically injected...
// You could just useDatabase(), everywhere....
// 👻
*/
Take a look through my Nightly Github Repo for more insight.
4. Next up Nitro Tasks: Database Migration Example
- Create a basic migration function inside
./server/utils/db.ts
like the one provided here:
import { drizzle } from "db0/integrations/drizzle/index";
export const orm = drizzle(useDatabase())
export function migrateDatabase() {
const db = useDatabase();
const orm = drizzle(useDatabase());
// simple and direct useDatabase() query
db.sql`CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
subject TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`;
};
- **Now lets run the migration with new experimental Nitro Tasks feature, basically schedulable cron jobs. Here's how you set it up declare and export a migration function inside
./server/utils/db.ts
.
import { drizzle } from "db0/integrations/drizzle/index";
export const orm = drizzle(useDatabase());
export function migrateDatabase() {
const db = useDatabase();
db.sql`CREATE TABLE IF NOT EXISTS tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
subject TEXT,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`;
}
- Create a new folder called
./server/tasks/db
and inside the new folder create a file calledmigrate.ts
with the following content:
export default defineNitroPlugin(async () => {
if (process.env.devDatabase) {
console.group('> Task', 'db:migrate')
const task = await runTask('db:migrate')
console.group(task.result)
}
})
- Create a new nitro server plugin called
init.ts
with the following content to fire off the nitro task.
export default defineTask({
async run() {
if (process.env.devDatabase) migrateDatabase()
return { result: 'ok' }
}
})
If you now check your terminal log, you'll see that Nitro automagically imports your init.ts
plugin and outputs the following:
ℹ Vite client warmed up in 932ms
ℹ Vite server warmed up in 1047ms
✔ Nuxt Nitro server built in 781 ms
> Task db:migrate
ok
If you see the above, the task has successfully migrated your database and created all the tables if they didn't exist yet. You can change or add more plugins or API handlers that trigger a Nitro Task. Read more about Nitro Tasks here.
Example Nitro Task Docs
export default defineTask({
meta: {
name: "db:migrate",
description: "Run database migrations",
},
run({ payload, context }) {
console.log("Running DB migration task...");
return "Success";
},
});
5. Hold up, What about my drizzle.config.ts
and typed SQL schema?
If you're working with drizzle-orm
and drizzle-kit
and a schema.ts
file like I have been, I have more good news, you don't need to change a thing. The experimental layers just saves you a ton of configuration time, each time you need it.
I recommend sticking with the ./server/database/schema.ts
and the drizzle.config.ts
route:
import type { Config } from "drizzle-kit";
export default {
schema: "server/database/schema.ts",
driver: "turso",
dbCredentials: {
url: process.env.TURSO_DB_URL as string,
authToken: process.env.TURSO_DB_AUTH_TOKEN as string,
},
} satisfies Config;
// or if your running devDatabase, don't forget the .env variable: devDatabase!
import { join } from 'pathe';
import type { Config } from "drizzle-kit";
export default {
out: 'server/database/migrations', // optional for migration folder
schema: 'server/database/schema.ts',
driver: process.env.devDatabase ? "better-sqlite" : "turso",
dbCredentials: {
url: process.env.devDatabase
? join(process.cwd(), './.data/db.sqlite3')
: process.env.TURSO_DB_URL as string,
authToken: process.env.devDatabase
? ""
: process.env.TURSO_DB_AUTH_TOKEN as string,
},
} satisfies Config;
You can then push your schema by running the following command in a second terminal:
// Push your schema
pnpm drizzle-kit push:sqlite --config=./drizzle.config.ts
// Launch Drizzle Studio to view your tables
pnpm drizzle-kit studio
Everything in your routes
and api
folder remains the same too. If you export the orm
helper like I did, it also gets automagically imported where ever you use it. The only thing that remains, is cleaner code.
You can still use Drizzle Studio inside and outside of the Dev Tools. The DX keeps multiplying 🤯.
6. Nuxt / Nitro API Example
/api/v1/tickets/index.ts
import { tickets, InsertTicket } from "~/server/database/schema";
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event);
const newTicket: InsertTicket = {
...body
}
const result = orm.insert(tickets).values(newTicket).run();
return { newTicket : result}
} catch (e: any) {
throw createError({
statusCode: 400,
statusMessage: e.message,
});
}
})
The experimental layer adds so much more fun on top the inherent fun that is working with Nitro inside and outside of Nuxt. The unjs
team is dropping bangers!
You can A/B compare both branches yourself.
That's the end of the article, I hope you picked up some tricks. Stay tuned for more awesome Nuxt articles!