· typescript / turso / sqlite
Hướng dẫn dùng Turso (libSQL) trong ứng dụng TypeScript
Thiết lập Turso libSQL trong TypeScript: cài @libsql/client, thực hiện CRUD, tích hợp Drizzle ORM và quản lý local dev — kèm những lỗi phổ biến nhất.
Bởi toolchew
2.014 từ · 11 phút đọc
Turso là cơ sở dữ liệu SQLite được host trên cloud, chạy được ở bất kỳ đâu TypeScript của bạn chạy — Node.js, Bun, Cloudflare Workers, Vercel Edge. Gói miễn phí cho phép 100 database, 500 triệu lượt đọc hàng, và 5 GB lưu trữ mỗi tháng. Đủ để vận hành một ứng dụng thực tế trên production mà không tốn đồng nào.
Bài viết này hướng dẫn bạn từ con số không đến một setup Turso hoàn chỉnh trong TypeScript, với Drizzle ORM tùy chọn bên trên. Mọi đoạn code ở đây đều chạy được thực sự — không có pseudocode nào cả.
Bài viết này dành cho ai
Các developer TypeScript muốn có một cơ sở dữ liệu SQLite nhẹ, chi phí thấp trên cloud. Bạn cần quen với async/await và có Node.js 18+ (hoặc Bun 1+) đã được cài đặt. Nếu bạn cần JSON path query phức tạp, PostGIS, hoặc write throughput rất cao, hãy ở lại với Postgres — xem phần những điểm khác biệt so với Postgres bên dưới.
Bước 1: Tạo database Turso
Qua CLI (khuyến nghị)
# Cài Turso CLI (macOS)
brew install tursodatabase/tap/turso
# Đăng nhập
turso auth login
# Tạo database
turso db create my-app-db
# Lấy URL kết nối
turso db show my-app-db
# Tạo auth token
turso db tokens create my-app-db
turso db show in ra URL có dạng libsql://my-app-db-[org].turso.io. turso db tokens create in ra token. Copy cả hai — bạn sẽ cần chúng ngay sau đây.
Qua web dashboard
Đăng ký tại turso.tech, nhấn New Database, chọn region, rồi copy URL và token từ dashboard. Dashboard cũng hiển thị số lượt đọc/ghi hàng, dung lượng lưu trữ, và các điểm restore PITR.
Bước 2: Cài @libsql/client
Với Node.js và Bun, cài @libsql/client. Với Cloudflare Workers và các edge runtime khác, Turso khuyến nghị dùng @tursodatabase/serverless thay thế — xem ghi chú về edge runtime ở Bước 3.
npm install @libsql/client
# hoặc
bun add @libsql/client
Lưu credentials của bạn:
# .env
TURSO_DATABASE_URL=libsql://my-app-db-[org].turso.io
TURSO_AUTH_TOKEN=your-token-here
Tên biến môi trường quan trọng. Nhiều tutorial dùng DATABASE_URL hay DATABASE_AUTH_TOKEN — cả hai đều sai. Hãy dùng TURSO_DATABASE_URL và TURSO_AUTH_TOKEN.
Bước 3: Kết nối
Client hỗ trợ ba URL scheme. Một package, một lời gọi createClient, ba chế độ:
import { createClient } from '@libsql/client';
// 1. In-memory — cho testing hoặc thử nhanh
const client = createClient({ url: ':memory:' });
// 2. File SQLite local — cho development
const client = createClient({ url: 'file:local.db' });
// Không cần authToken cho kết nối file:
// 3. Turso Cloud từ xa — cho production
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
Edge runtime (Cloudflare Workers, Vercel Edge Functions): Turso khuyến nghị dùng package @tursodatabase/serverless cho edge runtime. Cài riêng và import từ @tursodatabase/serverless/compat:
npm install @tursodatabase/serverless
// Cloudflare Workers và các edge runtime khác
import { createClient } from '@tursodatabase/serverless/compat';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
Đường dẫn /compat cung cấp cùng interface createClient như @libsql/client, nên phần còn lại của code bạn không cần thay đổi gì. Nó không hỗ trợ URL file:, nên local dev của bạn vẫn dùng import @libsql/client như bình thường.
Bước 4: CRUD với raw client
API cốt lõi là client.execute(). Truyền vào một chuỗi SQL thuần, hoặc một object với sql và args cho parameterized query. Luôn dùng parameterized query — đừng bao giờ nối input của người dùng vào chuỗi SQL.
import { createClient } from '@libsql/client';
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
// Tạo bảng
await client.execute(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0
)
`);
// INSERT
const insert = await client.execute({
sql: 'INSERT INTO todos (text) VALUES (?)',
args: ['Buy oat milk'],
});
console.log('new id:', insert.lastInsertRowid);
// SELECT tất cả
const { rows } = await client.execute('SELECT * FROM todos');
console.log(rows);
// SELECT một hàng
const { rows: [todo] } = await client.execute({
sql: 'SELECT * FROM todos WHERE id = ?',
args: [1],
});
// UPDATE
await client.execute({
sql: 'UPDATE todos SET done = 1 WHERE id = ?',
args: [1],
});
// DELETE
await client.execute({
sql: 'DELETE FROM todos WHERE id = ?',
args: [1],
});
Kết quả trả về có dạng { rows, columns, rowsAffected, lastInsertRowid }.
Batch và transaction
Dùng client.batch() khi bạn muốn thực thi tất cả-hoặc-không-gì-cả trên nhiều câu lệnh mà không cần kiểm soát tương tác:
await client.batch([
'INSERT INTO users (name) VALUES ("alice")',
{ sql: 'INSERT INTO orders (user_id) VALUES (?)', args: [1] },
], 'write');
Dùng client.transaction() khi bạn cần commit hoặc rollback dựa trên logic runtime:
const txn = await client.transaction('write');
try {
await txn.execute('UPDATE accounts SET balance = balance - 100 WHERE id = 1');
await txn.execute('UPDATE accounts SET balance = balance + 100 WHERE id = 2');
await txn.commit();
} catch (err) {
await txn.rollback();
throw err;
}
Các chế độ transaction: 'write' (đọc/ghi đầy đủ), 'read' (chỉ SELECT), 'deferred' (bắt đầu read, nâng lên write khi cần).
Bước 5: Drizzle ORM (khuyến nghị)
Drizzle là ORM phổ biến nhất khi dùng với Turso. Setup chỉ mất khoảng năm phút. Nếu bạn chưa chắc Drizzle có phải lựa chọn phù hợp không, xem bài Drizzle vs Kysely để so sánh hai công cụ query TypeScript phổ biến nhất hiện nay.
Cài đặt
npm install drizzle-orm @libsql/client dotenv
npm install -D drizzle-kit tsx
drizzle.config.ts
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'turso', // KHÔNG phải 'sqlite' — phải dùng 'turso'
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});
dialect: 'sqlite' là lỗi phổ biến nhất ở bước này. Nó sẽ âm thầm bỏ qua trường authToken. Hãy dùng 'turso'.
Schema
// src/db/schema.ts
import { int, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const usersTable = sqliteTable('users_table', {
id: int().primaryKey({ autoIncrement: true }),
name: text().notNull(),
age: int().notNull(),
email: text().notNull().unique(),
});
Kết nối database
// src/db/index.ts
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/libsql';
export const db = drizzle({
connection: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
},
});
CRUD với Drizzle
import { eq } from 'drizzle-orm';
import { db } from './db';
import { usersTable } from './db/schema';
// INSERT
await db.insert(usersTable).values({ name: 'Alice', age: 28, email: '[email protected]' });
// SELECT tất cả
const users = await db.select().from(usersTable);
// SELECT có điều kiện
const [user] = await db
.select()
.from(usersTable)
.where(eq(usersTable.email, '[email protected]'));
// UPDATE
await db
.update(usersTable)
.set({ age: 29 })
.where(eq(usersTable.email, '[email protected]'));
// DELETE
await db
.delete(usersTable)
.where(eq(usersTable.email, '[email protected]'));
Migration
# Tạo migration file
npx drizzle-kit generate
# Push schema trực tiếp (phím tắt cho dev — không tạo migration file)
npx drizzle-kit push
# Áp dụng migration file đã có
npx drizzle-kit migrate
Bước 6: Môi trường local
Ba lựa chọn, theo thứ tự bạn nên ưu tiên:
Lựa chọn A — File SQLite (đơn giản nhất). Thêm TURSO_DATABASE_URL=file:local.db vào .env.local và để createClient tự lấy. Không cần server, không cần CLI, hoạt động hoàn toàn offline. Đây là mặc định phù hợp cho hầu hết dự án.
Lựa chọn B — turso dev local server. Chạy một server libSQL local tại http://127.0.0.1:8080. Hữu ích khi bạn cần kiểm tra các tính năng đặc thù của libSQL (embedded replica sync, multi-database query) mà SQLite thuần không mô phỏng được.
# Tạm thời — dữ liệu mất khi tắt process
turso dev
# Lâu dài — dữ liệu được giữ sau khi restart
turso dev --db-file local.db
Việc mất dữ liệu khi dừng chế độ tạm thời là lỗi phổ biến nhất trong kênh hỗ trợ của Turso. Hãy luôn dùng --db-file trừ khi bạn muốn một database dùng một lần rồi bỏ.
Trỏ app của bạn về http://127.0.0.1:8080 với bất kỳ chuỗi nào (không rỗng) làm auth token cho local.
Lựa chọn C — Database remote trực tiếp. Trỏ dev trực tiếp vào một database Turso Cloud không phải production. Parity với production tốt nhất; nhưng không đáng đánh đổi bằng chi phí egress cloud cho công việc local thông thường.
Những điểm khác biệt so với Postgres
Kiểu dữ liệu
SQLite có ít kiểu dữ liệu native hơn Postgres. Đây là cách ánh xạ:
| Kiểu Postgres | Tương đương libSQL |
|---|---|
uuid | TEXT — dùng crypto.randomUUID() trong app |
jsonb | TEXT — parse trong app; không có JSON path operator |
ARRAY | Không hỗ trợ — dùng join table hoặc JSON text |
ENUM | TEXT với ràng buộc CHECK |
BOOLEAN | INTEGER (0/1) |
TIMESTAMPTZ | INTEGER (Unix ms) hoặc TEXT (ISO 8601) |
Write throughput
Turso được thiết kế cho workload nặng về đọc. Primary từ xa dùng mô hình single-writer, nên các workload ghi đồng thời cao — event log bận rộn, sổ cái thanh toán, lưu trạng thái game multiplayer — nên ở lại với Postgres.
Chế độ embedded replica của Turso giúp đọc local nhanh ở mức microsecond. Tốc độ đọc đó chỉ áp dụng cho replica local — ghi từ xa vẫn phải round-trip về primary.
Tính năng còn thiếu
- Không có stored procedure hay server-side function
- Không có
LISTEN/NOTIFY - Không có PostGIS hay hệ sinh thái extension
- Không có materialized view
ALTER TABLEgiới hạn — thêm cột thì được; đổi tên hay xóa cột cần tạo lại bảng- Full-text search qua FTS5 có nhưng kém mạnh hơn
tsvectorcủa Postgres
Điều Turso có mà SQLite thuần không có
- Truy cập qua mạng qua HTTP/WebSocket
- Nhiều database trên một org (100 miễn phí, không giới hạn trên gói trả phí)
- Point-in-time restore
- Vector search tích hợp sẵn — hữu ích cho workload embedding
- WAL mode luôn bật (không cần
PRAGMA journal_mode=WAL)
Giá và khi nào nên dùng
Tính đến tháng 6 năm 2026 (turso.tech/pricing):
| Gói | Giá | Số database | Lưu trữ | Lượt đọc/tháng | Lượt ghi/tháng |
|---|---|---|---|---|---|
| Free | $0 | 100 | 5 GB | 500M | 10M |
| Developer | $4.99/tháng | Không giới hạn | 9 GB | 2.5B | 25M |
| Scaler | $24.92/tháng | Không giới hạn | 24 GB | 100B | 100M |
| Pro | $416.58/tháng | Không giới hạn | 50 GB (+$0.45/GB) | 250B | 250M |
500 triệu lượt đọc của gói miễn phí là quá đủ cho hầu hết app nặng về đọc phục vụ vài nghìn người dùng hoạt động mỗi ngày.
Nếu bạn đang cân nhắc giữa Turso và Cloudflare D1, xem bài Turso vs Cloudflare D1 để có dữ liệu so sánh đầy đủ về latency, giá cả và tính năng.
Dùng Turso khi:
- Bạn đang xây dựng edge-first (Cloudflare Workers, Vercel Edge, Deno Deploy) và muốn độ trễ đọc thấp
- Bạn muốn cô lập database theo từng tenant — 100 database miễn phí là một con số hiếm có
- Bạn đang tiết kiệm chi phí và data model của bạn là CRUD đơn giản, không có kiểu dữ liệu đặc thù của Postgres
- Bạn muốn vector search tích hợp cho workload AI/embedding
Ở lại với Postgres khi:
- Bạn cần write throughput đồng thời cao (event log bận rộn, sổ cái thanh toán, lưu trạng thái multiplayer)
- Schema của bạn dùng
JSONBpath query,PostGIS, custom domain, hoặc logical replication - Team của bạn đã rất quen với hệ sinh thái Postgres và chi phí chuyển đổi không xứng với lợi ích về độ trễ edge