Slonik Transaction Middleware for Express.js

raw JSON →
3.2.0 verified Thu Apr 23 auth: no javascript

express-slonik provides an Express.js middleware for managing PostgreSQL transactions using the Slonik client library. It simplifies the integration of database transactions into Express route handlers, ensuring that all database operations within a request either commit successfully or roll back entirely. The current stable version is 3.2.0, released in October 2023. The project maintains a frequent release cadence, primarily to align with new major versions of its peer dependency, Slonik, adding support for recent Slonik versions shortly after their release. Its key differentiator is its zero-dependency approach (beyond Express and Slonik itself) and its focus on robust transaction management within the Express middleware pattern, including support for isolation levels and explicit transaction control across multiple handlers.

error TypeError: Cannot read properties of undefined (reading 'one')
cause `req.transaction` is undefined, typically because `transaction.begin()` middleware was not run or `slonik` peer dependency is missing/incorrect.
fix
Ensure transaction.begin() is used before the route handler that accesses req.transaction. Verify slonik is installed and meets the express-slonik peer dependency requirements (e.g., npm ls slonik or yarn why slonik).
error Error: Peer dependency "slonik" is not met. Expected ">=33.0.0 <38.0.0" but found "^32.0.0".
cause The installed `slonik` version does not satisfy the peer dependency range specified by `express-slonik`.
fix
Upgrade your slonik dependency to a compatible version, e.g., npm install slonik@^37 or yarn add slonik@^37.
error SyntaxError: Cannot use import statement outside a module
cause Attempting to use ES module `import` syntax in a CommonJS (`.js` without `"type": "module"` in `package.json`) Node.js environment.
fix
Either change your file to use CommonJS require() syntax (const createMiddleware = require('express-slonik').default;) or configure your project to use ES Modules by adding "type": "module" to your package.json and using .mjs file extensions if necessary.
breaking Version 3.0.0 introduced a breaking change by upgrading to Slonik v33. This change is not backward compatible with older Slonik versions due to API differences in Slonik itself.
fix Ensure your project's `slonik` dependency is at version `33.x.x` or higher when upgrading to `express-slonik` v3.x.x. Specifically, peer dependency `slonik` should be `>=33.0.0 <38.0.0` for `express-slonik@3.2.0`.
breaking Version 2.0.0 dropped support for Slonik versions 29 and below, requiring Slonik v30+.
fix Upgrade your `slonik` dependency to version `30.x.x` or higher before upgrading to `express-slonik` v2.x.x.
gotcha Transaction middleware relies on `req.transaction` being available. If other middleware modifies `req` in a conflicting way, it might cause issues.
fix Ensure `express-slonik` middleware is placed appropriately in your Express middleware chain so that `transaction.begin()` runs before route handlers that need `req.transaction`, and no subsequent middleware overwrites `req.transaction`.
gotcha Failing to explicitly call `transaction.end()` or having unhandled errors within a transactional route handler will automatically commit/rollback the transaction. While this is often desired, it can lead to unexpected behavior if manual control is intended.
fix For explicit control, always include `transaction.end()` after your route logic. Implement robust error handling (`try-catch`) to manage transaction outcomes precisely, especially for non-critical errors that shouldn't roll back the entire transaction.
npm install express-slonik
yarn add express-slonik
pnpm add express-slonik

This example demonstrates basic usage, setting up an Express application with Slonik, initializing express-slonik middleware, and handling a GET request within a database transaction.

import express from 'express';
import { createPool, sql, NotFoundError } from 'slonik';
import createMiddleware from 'express-slonik';
import { z } from 'zod';
import { Server } from 'http';

const userSchema = z.object({
  id: z.number().int(),
  name: z.string(),
  email: z.string().email(),
});

export const createExpressApp = ({ app, pool }) => {
  const transaction = createMiddleware(pool);

  app.get(
    '/user/:id',

    // Starts the transaction.
    transaction.begin(),

    async (req, res, next) => {
      try {
        const userId = parseInt(req.params.id, 10);
        if (isNaN(userId)) {
          return res.status(400).json({ message: 'Invalid user ID' });
        }

        const user = await req.transaction.one(
          sql.type(userSchema)`SELECT * FROM users WHERE users.id = ${userId}`
        );

        res.json(user);
      } catch (error) {
        if (error instanceof NotFoundError) {
          res.status(404).json({
            name: error.name,
            message: `User with given id (${req.params.id}) not found.`,
          });
          return;
        }

        next(error);
      }
    },

    // Optional: Explicitly commits the transaction. If omitted, it auto-commits on response send or rolls back on error.
    transaction.end()
  );

  return app;
};

/**
 * Gracefully attempt to shut down the server.
 */
async function shutdownHandler(server: Server, pool) {
  return async function () {
    server.close();
    await pool.end(); // Ensure the Slonik pool is also terminated
  };
}

(async function () {
  const app = express();
  // Ensure DATABASE_URL is set in your environment variables, e.g., 'postgres://user:pass@host:port/database'
  const pool = await createPool(process.env.DATABASE_URL ?? 'postgres://user:password@localhost:5432/mydb');
  const server = createExpressApp({ app, pool }).listen(8080, () => {
    console.log('Server listening on http://localhost:8080');
  });

  process
    .on('SIGTERM', shutdownHandler(server, pool))
    .on('SIGINT', shutdownHandler(server, pool));
})();