はじめに
ちょっとしたCLIツールの設定や、プロトタイプのデータ保存のためだけに、PostgreSQLやMongoDBを立ち上げるのは大げさすぎる——そう感じたことはないでしょうか。マイグレーションを書き、接続設定をして、Dockerを起動して、ようやくデータを1件保存できる。規模に見合わないセットアップコストに、心が折れそうになる場面は少なくありません。
Lowdbは、そんな「大きすぎるデータベース」への違和感に応えるライブラリです。JSONファイルやブラウザのlocalStorageをそのまま「DB」として扱い、db.dataにオブジェクトを読み書きするだけでデータを永続化できます。スキーマ定義もサーバーも不要で、npm installした瞬間から使い始められる手軽さが持ち味です。
とはいえ、説明を読むより触った方が理解が早いと思います。ブラウザだけで動くlocalStorage版のLowdbを用意したので、先に挙動を見たい方はこちらからどうぞ。
Lowdbとは
LowdbはtypicodeさんによるオープンソースのJavaScript製ローカルJSONデータベースです。JSON Serverやjson-serverと同じ作者によるもので、「小さく、型安全で、すぐ使える」ことを目指して設計されています。Node.js・Electron・ブラウザのいずれでも動作し、内部的にはアダプタと呼ばれる仕組みを差し替えることで、保存先をJSONファイルにもlocalStorageにもメモリにも変えられます。
主な特徴
- アダプタ方式で保存先を選べる - Node.jsでは
JSONFile、ブラウザではLocalStorageやSessionStorage、テスト用にはMemoryと、同じAPIのまま保存先だけを差し替えられます - APIが極小 - データは
db.dataにそのままオブジェクトとして載り、読み込みはdb.read()、書き込みはdb.write()の2つだけで完結します - TypeScriptとの相性が良い -
defaultDataに渡すオブジェクトの型がそのままdb.dataの型になるため、追加の型定義なしにスキーマの恩恵を受けられます - プレーンなJavaScriptで完結 - クエリ言語を覚える必要がなく、
Array.prototype.filterやfindなど普段のJS配列操作でそのままデータを検索・加工できます
インストール
npm install lowdb
yarn add lowdb
pnpm add lowdb
Lowdbのサンプルを動かす
下のサンプルは、LowdbのLocalStoragePresetを使ったToDoアプリです。ブラウザのlocalStorageをそのままデータベースとして使い、db.dataにタスクの配列を持たせています。テキストを入力して「追加」を押すとタスクが増え、「削除」ボタンで消せます。
要点だけを抜き出すと次のようになります。LocalStoragePreset(key, defaultData)が、指定したキーのlocalStorageを読み込み済みの状態でDBインスタンスを返してくれるので、これだけで永続化が完了します。
import { LocalStoragePreset } from 'lowdb/browser'
const db = LocalStoragePreset('todos', { todos: [] })
db.data.todos.push('Lowdbを試す')
db.write() // localStorageへ同期的に書き込む
console.log(db.data.todos)
実際に動かせるものが下です。追加・削除のたびにdb.write()が呼ばれ、ページを開き直しても(このプレイグラウンド内であれば)localStorageの内容が引き継がれます。
db.data.todos.push()でデータを直接書き換え、db.write()を呼んだ瞬間にlocalStorageへ反映される流れが分かるかと思います。配列操作の部分をfilterに変えれば削除ロジックを、findに変えれば特定タスクの検索を、普段のJavaScriptの書き方のまま実装できます。
基本的な使い方
Node.js環境では、lowdb/nodeが提供するJSONFilePresetを使うのが最もシンプルです。指定したパスのJSONファイルを読み込み、存在しなければdefaultDataで初期化した状態のDBインスタンスを返します。
import { JSONFilePreset } from 'lowdb/node'
// db.jsonを読み込む(なければ { posts: [] } で作成)
const db = await JSONFilePreset('db.json', { posts: [] })
// db.dataを直接書き換えてwrite()で保存
db.data.posts.push('Lowdbは最高')
await db.write()
// 明示的に再読み込みすることもできる
await db.read()
console.log(db.data.posts)
JSONFilePresetはPromiseを返す非同期版です。同期処理で完結させたい場合はJSONFileSyncPresetも用意されており、awaitなしでそのまま使えます。
実践的なユースケース
Node.jsでCLIツールの設定を永続化する
自作のCLIツールで、実行のたびにユーザー設定(APIキーやお気に入りのプロジェクト一覧など)を~/.myapp/config.jsonのようなファイルへ保存したいケースがあります。JSONFilePresetにファイルパスを渡すだけで、読み込み・保存の面倒な処理をLowdbに任せられます。
import { JSONFilePreset } from 'lowdb/node'
import os from 'node:os'
import path from 'node:path'
const configPath = path.join(os.homedir(), '.myapp', 'config.json')
const db = await JSONFilePreset(configPath, {
apiKey: null,
recentProjects: [],
})
db.data.apiKey = 'xxxx-xxxx'
db.data.recentProjects.unshift('my-project')
db.data.recentProjects = db.data.recentProjects.slice(0, 5) // 直近5件だけ保持
await db.write()
このパターンはNode.jsのファイルシステムに依存するため、ブラウザ上のサンプルは用意していません。手元のNode.js環境でぜひ試してみてください。
db.update()でブラウザの状態をまとめて更新する
先ほどのサンプルではdb.dataを直接書き換えてからdb.write()を呼びましたが、Lowdbには変更と保存を1回にまとめるdb.update(fn)というメソッドもあります。関数の中でdataを書き換えるだけで、保存漏れの心配がなくなります。
import { LocalStoragePreset } from 'lowdb/browser'
const db = LocalStoragePreset('notes', { notes: [] })
// updateの引数関数内でdataを書き換えると、自動でwrite()まで実行される
db.update((data) => {
data.notes.push({ text: 'Lowdbのupdateを試す', done: false })
})
下のサンプルはメモアプリです。チェックボックスで完了状態を切り替える処理を、すべてdb.update()経由で行っています。
db.update(fn)は内部でfn(this.data)を実行したあと自動的にwrite()を呼んでいるだけなので、「書き換え忘れて保存されない」というミスを防げます。チェックの切り替えと追加、どちらも同じupdate()で統一されている点に注目してください。
Memoryアダプタでテストやプロトタイピングを高速化する
永続化そのものが不要な場面もあります。単体テストでファイルI/Oやブラウザストレージを絡めたくない場合や、ページを開いている間だけ状態を持てれば十分なプロトタイプでは、Memoryアダプタが便利です。保存先が変数上のメモリになるだけで、db.data・db.write()のAPIはこれまでと同じです。
import { Low } from 'lowdb'
import { Memory } from 'lowdb'
const db = new Low(new Memory(), { count: 0 })
await db.read()
db.data.count++
await db.write()
下のサンプルはMemoryアダプタでカウンターを実装したものです。ページ(プレイグラウンド)を開いている間だけ値を保持し、リロードすると0に戻ります。
JSONFileやLocalStorageが「ファイル・localStorageへの書き込み」を行うのに対し、Memoryアダプタのwrite()は変数への代入だけで完結します。テストコードで本物のDBやストレージをモックする代わりに、このMemoryアダプタへ差し替えるだけで済むのも実用上のメリットです。
TypeScriptで型安全なスキーマを定義する
LowdbはdefaultDataに渡したオブジェクトの型を、そのままdb.dataの型として扱います。追加のスキーマ定義ファイルを書かなくても、TypeScriptの型チェックだけで不正なデータ操作を防げます。
import { JSONFilePreset } from 'lowdb/node'
type Data = {
messages: string[]
}
const defaultData: Data = { messages: [] }
const db = await JSONFilePreset<Data>('db.json', defaultData)
db.data.messages.push('foo') // OK
db.data.messages.push(1) // 型エラー: string型にnumberは代入できない
コンパイル時の型チェックが主眼のパターンなので、こちらは実行可能サンプルではなく静的なコード例として掲載しています。エディタ上でこのコードを書くと、push(1)の行にTypeScriptの型エラーが表示されるはずです。
まとめ
Lowdbは、JSONファイルやlocalStorageを「そのままDB」として扱える割り切りの良さが魅力のライブラリです。db.dataを読み書きしdb.write()を呼ぶだけというミニマルなAPIは、CLIツールの設定保存からブラウザ上のToDoアプリ、テスト用のインメモリストアまで幅広く応用できます。本格的なRDBMSを構える前の小さなプロジェクトやプロトタイピングの段階では、まずLowdbで始めてみるという選択肢を検討してみてはいかがでしょうか。