-
I'm really interested in Slonik as a database querying library, but I'm having a hard time understanding exactly how it would fit into a long-running back-end process like Express or Fastify. I'm hoping others who have figured out the right usage could share their insight. From what I can figure out, the best approach is to establish a connection pool when the Express/Fastify server is created, and then to ensure that the pool is accessible subsequently when the server is handling requests. Then, when individual endpoints are called by users, they should establish a specific connection from the already instantiated pool, make the query, and then do whatever they want with the results. Here's an example of what I think this looks like in Fastify:
import { FastifyPluginAsync } from 'fastify';
import fp from 'fastify-plugin';
import { createPool, DatabasePool } from 'slonik';
declare module 'fastify' {
interface FastifyInstance {
pool: DatabasePool;
}
}
export const slonikPlugin: FastifyPluginAsync = fp(async (server) => {
const pool = await createPool(process.env.DATABASE_URL || '');
server.decorate('pool', pool);
});
... [Build your server config]
fastify.register(slonikPlugin);
/** Route: GET /users
* Summary: Get list of users
* Details:
*/
fastify.route<{
Querystring: inputs.UserListOptions;
}>({
method: 'GET',
url: '/',
schema: {
querystring: inputs.userListOptions,
response: {
200: outputs.userList,
},
},
handler: async (request) => {
const users = await fastify.pool.connect(async (connection) => {
return connection.many(sql`SELECT * from "User" LIMIT 10`);
});
return { users }
}
}); Is this more or less the recommended approach, or am I missing a more obvious way to get the initial configuration up and running? Are there simple abstractions I could take advantage of so that each route handler doesn't have to repeat the connection functionality? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 7 replies
-
After noodling on this a bit more, I think I prefer a slightly different setup. This uses a class to instantiate a Slonik Connection singleton, which is a thin wrapper on existing Slonik methods. Here's an example:
import {
createPool,
DatabasePool,
TaggedTemplateLiteralInvocation,
} from 'slonik';
class Connection {
pool?: DatabasePool;
async getPool() {
if (!this.pool) {
this.pool = await createPool(process.env.DATABASE_URL || '');
if (!this.pool) {
throw new Error('Could not establish database connection pool');
}
}
return this.pool;
}
async maybeOne(sql: TaggedTemplateLiteralInvocation) {
const pool = await this.getPool();
return pool.connect(async (connection) => {
return connection.maybeOne(sql);
});
}
async many(sql: TaggedTemplateLiteralInvocation) {
const pool = await this.getPool();
return pool.connect(async (connection) => {
return connection.many(sql);
});
}
// And so forth for other slonik connection routes used in the app
}
export const connect = new Connection(); This allows for a much more readable set of code over in my route handlers: import { connect } from '../db/query.ts';
fastify.route<{
Querystring: inputs.UserListOptions;
}>({
method: 'GET',
url: '/',
schema: {
querystring: inputs.userListOptions,
response: {
200: outputs.userList,
},
},
handler: async (request) => {
const { query } = request;
const testUsers = await connect.many(sql`SELECT * from "User" LIMIT 10`);
return testUsers;
}
}); Or I could abstract related queries into a queries page: import { connect } from '../db/query.ts';
export const getUsers = () => {
return connect.many(sql`SELECT * from "User" LIMIT 10`);
};
import { getUsers } from './user-queries';
fastify.route<{
Querystring: inputs.UserListOptions;
}>({
method: 'GET',
url: '/',
schema: {
querystring: inputs.userListOptions,
response: {
200: outputs.userList,
},
},
handler: async (request) => {
const { query } = request;
const testUsers = await getUsers();
return testUsers;
}
}); I think this approach has two advantages over my first post:
Just sharing my approach here both for the benefit of anyone with a similar question, and because I'd appreciate feedback from any current Slonik users who may be using this or another approach to use Slonik within an API server. |
Beta Was this translation helpful? Give feedback.
-
I don't know about Fastify but I've created a middleware for Express.js for wrapping request handlers in a transaction. With express-slonik you can ensure your entire request is wrapped in a single database transaction which is really useful when you want to ensure all of the handlers and middleware are properly isolated for a given request context: import express, { json } from "express";
import createMiddleware, { IsolationLevels } from "express-slonik";
// middleware/currentUser.ts
// Middleware that gets the current user from database from request session data.
const currentUser = function () {
return async function (req, res, next) {
try {
req.currentUser = await req.transaction.one(
sql`SELECT * FROM users WHERE users.id = ${req.session.userId}`
);
next()
} catch (error) {
next(error);
}
}
};
// middleware/checkPermission.ts
// Middleware to check whether current user has permission to perform this action
const checkPermission = function (permissionName: string) {
return async function (req, res, next) {
try {
const permissions = await req.transaction.manyFirst(sql`
SELECT permissions.name
FROM roles
INNER JOIN permissions ON permissions.role_id = roles.id
INNER JOIN user_roles ON user_roles.role_id = roles.id
WHERE user_roles.user_id = ${req.session.userId}
`);
)
} catch (error) {
next(error);
}
};
};
// app.ts
(async function () {
const pool = await createPool(process.env.DATABASE_URL);
const app = express();
const transaction = createMiddleware(pool);
app
.use(json())
.patch(
"/post/:id",
// Start a transaction context (can also specify isolation levels if needed)
transaction.begin(),
// Since currentUser and checkPermissions are wrapped in the transaction, we know that
// nothing can modify the user's permissions outside of current request context.
currentUser(),
checkPermission("update:post"),
// By the time we get to the request handler, we have sufficient guarantees that the user in
// the current request context has the permission to update the post and that no one else
// is updating this post at the same time.
async (req, res, next) => {
try {
await req.transaction.query(sql`
UPDATE posts SET
posts.updated_by = ${req.session.userId},
posts.updated_at = now(),
-- Don't do this at production kids, we want the content sanitized!
posts.content = ${req.body.content}
WHERE posts.id = ${req.params.id}`
);
const updatedPost = await req.transaction.one(
sql`SELECT * FROM posts WHERE posts.id = ${req.params.id}`
);
// Respond with updated post JSON
res.json(updatedPost);
} catch (error) {
next(error);
}
},
// By default, express-slonik autocommits the transaction when a response is sent to the client
// by Express.js or autorollbacks whenever an error is detected in the request handler chain.
// You can also be explicit or have a finer control on when this happens by uncommenting the
// middleware below:
// transaction.end()
// Middleware inserted after transaction.end will not have access to req.transaction context and
// will need to begin a new transaction context!
})(); Checkout the README for more documentation and examples. As always, I would like to know what you guys think about this pattern for Express.js apps. |
Beta Was this translation helpful? Give feedback.
-
Two articles on the subject: |
Beta Was this translation helpful? Give feedback.
Two articles on the subject: