Recipes

Testing

A database built from your app's own config for the test suite, shared by the run or isolated per test, without touching the app's data.

A test suite needs a database that matches the app's: the same extensions, the same schema, the same seed. nuxt-pglite/testing builds one from your server config, init included, so migrations applied from init with applyMigrations are applied here too. It lives in memory, so nothing of the app's data directory is touched, and it works with vitest, @nuxt/test-utils or any other runner.

test/todos.test.ts
import { afterAll, beforeAll, expect, it } from 'vitest'
import { createTestDatabase } from 'nuxt-pglite/testing'
import type { TestDatabase } from 'nuxt-pglite/testing'

let db: TestDatabase

beforeAll(async () => {
  db = await createTestDatabase({ config: 'server/pglite.config' })
})
afterAll(() => db.close())

it('stores a todo', async () => {
  await db.pg.query('INSERT INTO todos (title) VALUES ($1)', ['first'])
  const { rows } = await db.pg.query('SELECT title FROM todos')
  expect(rows).toEqual([{ title: 'first' }])
})

db.pg is the PGlite instance, after init has run. close() runs the config's dispose, then closes everything the database opened: its forks, its socket and the instance.

One database can serve a whole suite, or be forked per test so that each test starts from the same data. For a whole vitest run behind one URL, see vitest.

Options

const db = await createTestDatabase({
  config: 'server/pglite.config', // or the config object itself
  alias: {}, // import aliases the config file uses, see below
  dataDir: 'memory://', // whatever the config says
  socket: false, // `true`, or the socket's options, to serve it over a URL
  exportEnv: true, // when `socket` is on: set its variables on `process.env`
})
OptionTypeDefault
configPGliteConfig | string{}The app's server config, or the path of its file. $test overrides apply.
aliasRecord<string, string>Import aliases a config path may use, name to absolute path, as nuxt.options.alias holds them. See Config files.
dataDirstring'memory://'Replaces the config's dataDir. The config's fs is dropped too, since it would hold the app's own data directory.
socketboolean | TestSocketOptionsfalseServes the database over the Postgres wire protocol: true for a free loopback port. See Through the socket.
exportEnvbooleantrue when socket is onSets the socket's variables on process.env while the database is open, those still unset only.

Passed as an object, config types db.pg with its extensions; loaded from a path, it cannot.

Config files

A config path is loaded with loadPGliteConfig(path, { alias }), also exported, the way the module loads it:

  • relative to process.cwd(), extension optional;
  • imported through jiti, so TypeScript works on any runtime, and fresh on every call;
  • with definePGliteServerConfig provided as a global while the file is imported, so a file written for the auto-import works unchanged;
  • with the $test overrides applied, and $development / $production ignored.
server/pglite.config.ts
export default definePGliteServerConfig({
  init: async (pg) => {
    await pg.exec('CREATE TABLE IF NOT EXISTS todos (id serial PRIMARY KEY, title text NOT NULL)')
  },
  $test: {
    debug: 1, // a shallow override, applied in tests only
  },
})

Outside Nuxt the app's aliases (~~, #pglite/migrations, …) are not defined, so a file importing through one fails to load. Either import from nuxt-pglite/migrations in the file, which resolves anywhere, or pass the aliases as alias, name to absolute path:

import { fileURLToPath } from 'node:url'

const db = await createTestDatabase({
  config: 'server/pglite.config',
  alias: {
    '#pglite/migrations': fileURLToPath(import.meta.resolve('nuxt-pglite/migrations')),
  },
})

The paths of .nuxt/tsconfig.json list the others.

Through the socket

Code that connects through a URL, rather than receiving pg, reaches the database over the Postgres wire protocol with socket: true for a free loopback port, or the module's socket options but devtoolsActions (port, env, provider, …), plus the server's own user, database and logger.

const db = await createTestDatabase({ config: 'server/pglite.config', socket: true })

db.url // postgres://postgres@127.0.0.1:<port>/postgres
db.env // { DATABASE_URL: db.url }

db.env holds the variables the socket resolves, as in nuxt dev: DATABASE_URL by default, NETLIFY_DB_URL and NETLIFY_DB_DRIVER with provider: 'netlify', env merged over them. They are set on process.env while the database is open, unless exportEnv: false, and unset on close().

As with the development socket, each variable is set only while still unset. With a DATABASE_URL already in the environment, from your shell or a CI job, code reading it reaches that database rather than the test one. db.env always holds the test database's values, exported or not: pass it on explicitly when that matters.

Isolation

Fork a seeded database per test.

vitest

One database for the whole run, from a globalSetup file.

@nuxt/test-utils

Boot the built app against a test database.