Extending Prisma ORM with Custom Middleware: Logging, Validation, and Security
Tags: prisma, middleware, prismaorm, node
Published: 2024-07-13 15:27:10

#### What Will This Blog Cover?

In this blog, we will:

1. Explain what middleware is and how it works in Prisma ORM.
2. Show how to create middleware for logging, validation, and security.
3. Provide examples of how to use middleware in your projects.

By the end of this blog, you’ll understand how to extend Prisma ORM’s functionality with custom middleware to make your applications more robust and secure.

## Section 1: Understanding Prisma Middleware

#### What is Middleware in Prisma?

Middleware in Prisma is a piece of code that runs before or after a database query. It acts like a checkpoint where you can add extra tasks, such as logging, validation, or security checks.

#### How Does Middleware Work with Prisma’s Request Lifecycle?

When you make a database request with Prisma, the request goes through several stages. Middleware can be added at these stages to perform additional actions. This helps you control and modify the behavior of your database interactions.

#### Overview of Middleware Use Cases

Middleware can be used for various purposes:

- Logging: Keep a record of all database queries and their execution times.
- Validation: Ensure that the data being saved meets specific criteria.
- Security: Implement checks to protect your data from unauthorized access.

## Section 2: Setting Up Your Prisma Project

#### Basic Prisma Setup

Before you start using middleware, you need to have a basic Prisma setup. This includes having Node.js installed and a database to connect to.

#### Guide to Initializing a Prisma Project


1.**Install Prisma CLI:** Open your terminal and run:
`npm install @prisma/cli --save-dev`

2.**Initialize Prisma:**Set up Prisma in your project by running:
`npx prisma init`

3.**Install Prisma Client:** Add the Prisma Client to your project with:
`npm install @prisma/client`

4.**Configure the Database:** Update the prisma/schema.prisma file to define your data models and connect to your database.

With these steps, your Prisma project will be ready to use, and you can start adding middleware to enhance its functionality.

## Section 3: Creating a Custom Middleware

#### 3.1 Basic Middleware Structure

**What is the Basic Structure of a Middleware Function in Prisma?**

A middleware function in Prisma is a piece of code that runs every time a database query is made. It has two main parts:

- Parameters: Information about the query being made.
- Next Function: A function that allows the query to proceed to the database.

**Example of a Simple Middleware to Log Queries**

Here’s how you can create a middleware that logs every query:

**1.Create the Middleware Function:**

```javascript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

   prisma.$use(async (params, next) => {
     // Log the query parameters
     console.log('Params:', params);
     
     // Execute the query
     const result = await next(params);
     
     // Log the result
     console.log('Result:', result);
     
     return result;
   });
```

**2.Explanation:**

- Params: Contains information about the model and action of the query.
- Next: A function that allows the query to be executed.
- Logging: Logs the parameters before executing the query and the result after the query.

This simple middleware logs every query made to the database, helping you keep track of what’s happening in your application.

##Section 4: Adding Custom Validation

**4.1 Why Custom Validation?**

**Importance of Custom Validation**

Custom validation is crucial because it ensures that the data being saved to your database meets specific requirements and standards. It helps in maintaining data integrity and preventing invalid or malicious data from being stored.

Use Cases Where Built-in Validation is Insufficient

- Complex Data Structures: When you need to validate nested objects or arrays.
- Conditional Validation: When validation rules depend on other fields’ values.
- Custom Business Logic: When you have specific business rules that need to be enforced, which built-in validation rules can’t handle.

**4.2 Implementing Validation Middleware**

Creating Middleware to Validate Data

Here’s how you can create middleware to validate data before it reaches the database:

**1.Create the Validation Middleware Function:**

```javascript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

prisma.$use(async (params, next) => {
  // Custom validation for the 'User' model on create action
  if (params.model === 'User' && params.action === 'create') {
    const { email, password } = params.args.data;
    
    // Example validation: Check if email contains '@'
    if (!email.includes('@')) {
      throw new Error('Invalid email format');
    }
    
    // Example validation: Check if password length is at least 8 characters
    if (password.length < 8) {
      throw new Error('Password must be at least 8 characters long');
    }
  }
  
  // Proceed with the query
  return next(params);
});
```
This middleware validates the data for the ‘User’ model before it is saved to the database, ensuring that only valid data is stored.

## Section 5: Implementing Security Checks

**5.1 Importance of Security Middleware**

**Why Security Checks are Critical**

Security checks are essential to protect your application and its data from unauthorized access and malicious activities. They ensure that only authorized users can perform certain actions, thereby safeguarding sensitive information and maintaining data integrity.

Common Security Concerns

- Authorization: Ensuring that users have the right permissions to access or modify data.
- Data Access Control: Restricting access to certain data based on user roles or other criteria.

**5.2 Creating Security Middleware**

Example of Middleware to Enforce User Permissions

Here’s how you can create middleware to enforce user permissions:

**1.Create the Security Middleware Function:**

```javascript
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

prisma.$use(async (params, next) => {
  // Security check for the 'Post' model on update and delete actions
  if (params.model === 'Post' && (params.action === 'update' || params.action === 'delete')) {
    const postId = params.args.where.id;
    
    // Fetch the post to check the author
    const post = await prisma.post.findUnique({ where: { id: postId } });
    
    // Replace 'getCurrentUserId' with actual implementation to get current user's ID
    if (post.authorId !== getCurrentUserId()) {
      throw new Error('You are not authorized to perform this action');
    }
  }
  
  // Proceed with the query
  return next(params);
});

// Dummy function to simulate fetching the current user's ID
function getCurrentUserId() {
  return 1; // Replace with actual implementation
}
```

This middleware enforces user permissions, ensuring that only the author of a post can update or delete it, thereby enhancing the security of your application.

## Section 6: Combining Middleware

#### How to Combine Multiple Middleware Functions
Combining multiple middleware functions in Prisma allows you to perform several tasks in sequence, such as logging, validation, and security checks. Prisma executes middleware in the order they are defined, making it easy to chain multiple middleware functions together.

#### Example of Using Multiple Middleware for Logging, Validation, and Security
Here's how you can combine middleware functions to handle logging, validation, and security:

1. **Define Middleware Functions**:
   ```javascript
   const { PrismaClient } = require('@prisma/client');
   const prisma = new PrismaClient();

   // Logging middleware
   const loggingMiddleware = async (params, next) => {
     console.log('Params:', params);
     const result = await next(params);
     console.log('Result:', result);
     return result;
   };

   // Validation middleware
   const validationMiddleware = async (params, next) => {
     if (params.model === 'User' && params.action === 'create') {
       const { email, password } = params.args.data;
       if (!email.includes('@')) {
         throw new Error('Invalid email format');
       }
       if (password.length < 8) {
         throw new Error('Password must be at least 8 characters long');
       }
     }
     return next(params);
   };

   // Security middleware
   const securityMiddleware = async (params, next) => {
     if (params.model === 'Post' && (params.action === 'update' || params.action === 'delete')) {
       const postId = params.args.where.id;
       const post = await prisma.post.findUnique({ where: { id: postId } });
       if (post.authorId !== getCurrentUserId()) {
         throw new Error('You are not authorized to perform this action');
       }
     }
     return next(params);
   };

   function getCurrentUserId() {
     return 1; // Replace with actual implementation
   }
   ```

2. **Combine and Use Middleware Functions**:
   ```javascript
   // Adding middleware to Prisma client
   prisma.$use(loggingMiddleware);
   prisma.$use(validationMiddleware);
   prisma.$use(securityMiddleware);
   ```

3. **Explanation**:
   - **Logging Middleware**: Logs the parameters and results of each query.
   - **Validation Middleware**: Validates user data before it is saved to the database.
   - **Security Middleware**: Ensures that only authorized users can update or delete posts.
   - **Combining Middleware**: Middleware functions are added to the Prisma client in the desired order, ensuring they run sequentially.

By combining these middleware functions, you can create a robust Prisma setup that handles multiple aspects of query processing, from logging and validation to security, making your application more reliable and secure.

## Section 7: Testing Your Middleware

#### Importance of Testing Middleware
Testing middleware ensures it works correctly, prevents bugs, and maintains code quality.

#### Tips for Testing Middleware
- **Isolate Logic**: Test middleware separately from other parts of the application.
- **Mock Dependencies**: Simulate database interactions and other external dependencies.
- **Check Edge Cases**: Ensure the middleware handles all possible scenarios, including edge cases and error conditions.