{"id":16323786,"url":"https://github.com/andrewjo/express-slonik","last_synced_at":"2025-10-25T20:31:10.974Z","repository":{"id":57686224,"uuid":"475832761","full_name":"AndrewJo/express-slonik","owner":"AndrewJo","description":"Slonik transaction middleware with zero dependencies.","archived":false,"fork":false,"pushed_at":"2023-10-16T21:14:13.000Z","size":508,"stargazers_count":3,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-31T10:23:14.115Z","etag":null,"topics":["database","database-transactions","express","expressjs","middleware","nodejs","postgres","postgres-transactions","postgresql","slonik","transactions"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndrewJo.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-30T10:42:10.000Z","updated_at":"2023-05-11T06:47:35.000Z","dependencies_parsed_at":"2023-02-17T02:46:16.585Z","dependency_job_id":null,"html_url":"https://github.com/AndrewJo/express-slonik","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fexpress-slonik","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fexpress-slonik/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fexpress-slonik/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewJo%2Fexpress-slonik/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndrewJo","download_url":"https://codeload.github.com/AndrewJo/express-slonik/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238207641,"owners_count":19434095,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","database-transactions","express","expressjs","middleware","nodejs","postgres","postgres-transactions","postgresql","slonik","transactions"],"created_at":"2024-10-10T22:55:40.301Z","updated_at":"2025-10-25T20:31:05.647Z","avatar_url":"https://github.com/AndrewJo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# express-slonik\n\n[![npm](https://img.shields.io/npm/v/express-slonik?style=flat-square)][npm]\n[![CircleCI](https://img.shields.io/circleci/build/github/AndrewJo/express-slonik/master?style=flat-square)][circleci]\n[![Codecov branch](https://img.shields.io/codecov/c/github/AndrewJo/express-slonik/master?style=flat-square)][codecov]\n[![GitHub](https://img.shields.io/github/license/AndrewJo/express-slonik?style=flat-square)](./LICENSE)\n[![npm](https://img.shields.io/npm/dw/express-slonik?style=flat-square)][npm]\n\n[Slonik][slonik] transaction middleware for [Express.js][expressjs] with zero dependencies.\n\n## Table of Contents\n\n- [Getting started](#getting-started)\n- [Usage](#usage)\n  - [Basic usage](#basic-usage)\n  - [Sharing transaction with multiple route handlers or middleware](#sharing-transaction-with-multiple-route-handlers-or-middleware)\n  - [Setting isolation levels](#setting-isolation-levels)\n- [Version compatibility](#version-compatibility)\n- [Other projects](#other-projects)\n\n## Getting started\n\nInstall the middleware as a dependency in your [Express.js](https://expressjs.com) project:\n\n```shell\nnpm i -S slonik express-slonik\n```\n\n## Usage\n\nYou can use the `createMiddleware` function to create a request transaction context that contains\nmethods to wrap your route handlers in a PostgreSQL transaction.\n\n### Basic usage\n\nUse the `transaction.begin()` and `transaction.end()` middleware to wrap your request handlers in a\ntransaction.\n\n**`app.ts`**:\n\n```typescript\nimport createMiddleware from \"express-slonik\";\nimport { sql } from \"slonik\";\nimport { z } from \"zod\";\n\nconst userSchema = z.object({\n  id: z.number().int(),\n  name: z.string(),\n  email: z.string().email(),\n});\n\nexport const createServer = ({ app, pool }) =\u003e {\n  const transaction = createMiddleware(pool);\n\n  app.get(\n    \"/user/:id\",\n\n    // Starts the transaction.\n    transaction.begin(),\n\n    async (req, res, next) =\u003e {\n      try {\n        const user = await req.transaction.one(\n          sql.type(\n            userSchema\n          )`SELECT * FROM users WHERE users.id = ${req.params.id}`\n        );\n\n        res.json(user);\n      } catch (error) {\n        if (error instanceof NotFoundError) {\n          res.status(404).json({\n            name: error.name,\n            message: `User with given id (${req.params.id}) not found.`,\n          });\n          return;\n        }\n\n        next(error);\n      }\n    },\n\n    // Optional. If omitted, the transaction will automatically commit when the\n    // response is sent, or rollback if there are unhandled errors. Specifying\n    // transaction.end() is useful if you wish to have finer control over when\n    // the transaction commits in the middleware chain.\n    transaction.end()\n  );\n\n  const server = app.listen(8080);\n\n  // Cleanup when server closes or you might have something that keeps the process running.\n  server.on(\"close\", async function () {\n    await pool.end();\n  });\n\n  return server;\n};\n```\n\n**`server.ts`**:\n\n```typescript\nimport { Server } from \"http\";\nimport express from \"express\";\nimport { createPool } from \"slonik\";\nimport { createServer, userSchema } from \"./app\";\n\n/**\n * Gracefully attempt to shut down the server.\n */\nasync function shutdownHandler(server: Server) {\n  // Handler that triggers a graceful shutdown. Server is responsible for cleaning up stragglers.\n  return async function () {\n    server.close();\n  };\n}\n\n(async function () {\n  const app = express();\n  const pool = await createPool(process.env.DATABASE_URL);\n  const server = createServer({ app, pool });\n\n  process\n    .on(\"SIGTERM\", shutdownHandler(server))\n    .on(\"SIGINT\", shutdownHandler(server));\n})();\n```\n\nThis is functionally equivalent to using `pool.transaction` in your handler:\n\n```typescript\napp.get(\"/user/:id\", async (req, res, next) =\u003e {\n  try {\n    const user = await pool.transaction(async (transaction) =\u003e {\n      return await transaction.one(\n        sql.type(\n          userSchema\n        )`SELECT * FROM users WHERE users.id = ${req.params.id}`\n      );\n    });\n\n    res.json(user);\n  } catch (error) {\n    if (error instanceof NotFoundError) {\n      res.status(404).json({\n        name: error.name,\n        message: `User with given id (${req.params.id}) not found.`,\n      });\n      return;\n    }\n\n    next(error);\n  }\n});\n```\n\n### Sharing transaction with multiple route handlers or middleware\n\nSuppose you had a middleware that returns the current user from the session or JWT. You can make\nsure the user edit handler are on the same database transaction as the current user middleware.\nThis can prevent concurrent user updates from causing inconsistent the query result between the\ntime the current user middleware and your user edit handler executes.\n\n**`schemas/user.ts`**:\n\n```typescript\nimport { z } from \"zod\";\nexport const userSchema = z.object({\n  id: z.number().int(),\n  name: z.string(),\n  email: z.string().email(),\n});\n```\n\n**`middleware/current-user.ts`**:\n\n```typescript\nimport { userSchema } from \"schemas/user\";\n\nexport default function currentUser() {\n  return async function (req, res, next) {\n    try {\n      req.currentUser = await req.transaction.one(\n        sql.type(\n          userSchema\n        )`SELECT * FROM users WHERE users.id = ${req.session.userId}`\n      );\n      next();\n    } catch (error) {\n      next(error);\n    }\n  };\n}\n```\n\n**`app.ts`**:\n\n```typescript\nimport express, { json } from \"express\";\nimport createMiddleware from \"express-slonik\";\nimport { createPool, sql } from \"slonik\";\nimport currentUser from \"./middleware/current-user\";\nimport { userSchema } from \"./schemas/user\";\n\nconst pool = createPool(\"postgres://localhost:5432/example_db\");\nconst transaction = createMiddleware(pool);\nconst app = express();\n\napp\n  .use(json())\n  .put(\n    \"/user/:id\",\n    transaction.begin(),\n    currentUser(),\n    async (req, res, next) =\u003e {\n      try {\n        // Same transaction as currentUser middleware\n        await req.transaction.query(\n          sql.type(\n            z.object({})\n          )`UPDATE users SET email = ${req.body.email} WHERE users.id = ${req.params.id}`\n        );\n\n        const updatedUser = await req.transaction.one(\n          sql.type(\n            userSchema\n          )`SELECT * FROM users WHERE users.id = ${req.params.id}`\n        );\n        res.json(updatedUser);\n      } catch (error) {\n        next(error);\n      }\n    },\n    transaction.end()\n  )\n  .use((error, req, res, next) =\u003e {\n    res.status(401).end();\n  });\n\napp.listen(8080);\n```\n\nThis behavior is especially helpful when you are using a custom validator or sanitizor in libraries\nlike [express-validator](https://express-validator.github.io/):\n\n```typescript\nimport express, { json } from \"express\";\nimport createMiddleware from \"express-slonik\";\nimport { body, validationResult } from \"express-validator\";\nimport { createPool, sql } from \"slonik\";\nimport currentUser from \"./middleware/current-user\";\nimport { teamSchema } from \"./schemas/team\";\nimport { userSchema } from \"./schemas/user\";\n\nconst pool = createPool(\"postgres://localhost:5432/example_db\");\nconst transaction = createMiddleware(pool);\nconst app = express();\n\napp\n  .use(json())\n  .put(\n    \"/user/:id\",\n    transaction.begin(),\n    body(\"email\").isEmail().normalizeEmail(),\n    body(\"team_id\")\n      .toInt()\n      .custom(async (value, { req }) =\u003e {\n        // Fail validation if client is attempting to add user to a non-existant team\n        const isValidTeam = await req.transaction.exists(\n          sql.type(\n            teamSchema\n          )`SELECT * FROM teams WHERE teams.id = ${req.body.team_id}`\n        );\n\n        if (!isValidTeam) {\n          throw new Error(\"Invalid team\");\n        }\n      }),\n    async (req, res, next) =\u003e {\n      const errors = validationResult(req);\n\n      if (!errors.isEmpty()) {\n        res.status(422).json(errors.array());\n        return;\n      }\n\n      // We can assume the request body is valid and sanitized by the time we reach this point.\n      try {\n        await req.transaction.query(sql.unsafe`\n          UPDATE users SET\n            email = ${req.body.email},\n            team_id = ${req.body.team_id}\n          WHERE\n            users.id = ${req.params.id}\n        `);\n\n        const user = await req.transaction.one(\n          sql.type(\n            userSchema\n          )`SELECT * FROM users WHERE users.id = ${req.params.id}`\n        );\n\n        res.status(200).json(user);\n      } catch (error) {\n        next(error);\n      }\n    },\n    transaction.end()\n  )\n  .use((error, req, res, next) =\u003e {\n    res.status(401).end();\n  });\n\napp.listen(8080);\n```\n\n### Setting isolation levels\n\nThe `transaction.begin` can take an optional argument to specify transaction isolation levels. It\ndefaults to READ COMMITTED isolation level is left empty.\n\nThere are three isolation levels: `READ COMMITTED`, `REPEATABLE READ`, and `SERIALIZABLE`.\n\n```typescript\nimport createMiddleware, { IsolationLevels } from \"express-slonik\";\nimport { createPool } from \"slonik\";\n\nconst transaction = createMiddleware(\n  createPool(\"postgres://localhost:5432/example_db\")\n);\n\napp.get(\n  \"/posts/:postId/comments\",\n  transaction.begin(IsolationLevels.SERIALIZABLE),\n  // ...\n  transaction.end()\n);\n```\n\nFor more information on the differences between transaction isolation levels, please refer to:\n[13.2. Transaction Isolation — PostgreSQL documentation](https://www.postgresql.org/docs/current/transaction-iso.html).\n\n## Version compatibility\n\nexpress-slonik follows [Semantic Versioning][semver] specification. Each major version breaks\nbackwards compatibility with [Slonik][slonik] and [Express.js][expressjs] versions (although\nExpress v5 has been extremely slow to come out of beta).\n\nRefer to the compatibility chart below for picking the express-slonik version that works with Slonik\nversions in your project.\n\n| express-slonik |                            slonik |\n| -------------: | --------------------------------: |\n|         ^3.0.0 |              ^33.0.0 \\|\\| ^34.0.0 |\n|         ^2.0.0 | ^30.0.0 \\|\\| ^31.0.0 \\|\\| ^32.0.0 |\n|         ^1.1.0 |              ^28.0.0 \\|\\| ^29.0.0 |\n|  ≥1.0.0 \u003c1.1.0 |                           ^28.0.0 |\n\nMinor version will always add support for Slonik versions that doesn't introduce major backwards\nincompatibility that breaks interoperability with this library. For instance, the breaking changes\nintroduced between Slonik v28 and v29 are fairly minor and can be used with express-slonik without\nany major refactor to how you use this middleware. However, the difference between v29 and v30\nintroduces a major change to the API surface (which also affects\n[other Slonik utility packages][slonik-tools-issue-407]). In this case, the major version of\nexpress-slonik will be bumped up to indicate that there will be backwards breaking changes.\n\n## Other projects\n\nNeed to isolate database calls in your tests? Check out [mocha-slonik][mocha-slonik]!\n\n[npm]: https://www.npmjs.com/package/express-slonik\n[circleci]: https://circleci.com/gh/AndrewJo/express-slonik/tree/master\n[codecov]: https://app.codecov.io/gh/AndrewJo/express-slonik/\n[slonik]: https://github.com/gajus/slonik\n[expressjs]: https://expressjs.com\n[semver]: https://semver.org/\n[slonik-tools-issue-407]: https://github.com/mmkal/slonik-tools/issues/407\n[mocha-slonik]: https://github.com/AndrewJo/mocha-slonik\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewjo%2Fexpress-slonik","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrewjo%2Fexpress-slonik","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewjo%2Fexpress-slonik/lists"}