How can I separate data layer from Next.js API routes?

1.1k views Asked by At

I am using Next.js with MongoDB, Currently I am using the MongoDB client itself in TypeScript. I was wondering if later on I have to change my database, I need to customize the api/route.ts file.

Instead of directly customizing the route.ts file can we inject some kind of dependency that just deals with data operation and abstracts out this database part in a separate file?

Currently, my api/route.ts file looks like below, as you can see it's tightly coupled with vendor-specific database API.

import clientPromise from '@/app/mongodb';
import { NextResponse } from 'next/server';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from "uuid";

export async function POST(request: Request) {
    const ip = // get the IP
    const sha256 = crypto.createHmac("sha256", process.env.SALT!);
    const ipHash = sha256.update(ip!);
    const ipHashDigest = ipHash.digest("hex");

    const client = await clientPromise;
    const db = client.db("survey-db");
    const creatorHistory = await db.collection("creator-ip-hash").findOne({ ipHash: ipHashDigest });

    const uuid = uuidv4();

    if (creatorHistory === null) {
        await db.collection("creator-ip-hash").insertOne({ ipHash: ipHashDigest, count: 1 });
    } else {
        const prevCount = creatorHistory.count;

        if (prevCount >= 3) {
            return NextResponse.json({ "error": "API Limit is reached" }, { status: 429 });
        }

        await db.collection("creator-ip-hash").updateOne({ ipHash: ipHashDigest }, { $set: { count: prevCount + 1 } });
    }

    const survey = await request.json();
    survey.creatorIpHash = ipHashDigest;
    survey.uuid = uuid;
    await db.collection("survey-templates").insertOne(survey);

    return NextResponse.json({ "survey-id": uuid }, { status: 201 });
}

I know POST is not a class but a function, but still there is a way to inject dependency somehow. I was looking at this guide: https://himynameistim.com/blog/dependency-injection-with-nextjs-and-typescript but it seems like it for maybe an older version and also doesn't show how to incorporate this in API routes. They have shown the use of inject and injectable but it's for class.

I found this discussion on the next.js GitHub community: https://github.com/vercel/next.js/discussions/46805

Seems like we have to work around with package.json or webpack, Still I can't able to find any proper guide on how to use this in API routes. They are using tsyringe for the dependency injection container.

However, I am open to any solution just decoupling this data access layer from API routes, so in the future, if I have to change the database backend I can do it efficiently, if anyone has done it before please let me know your idea.

3

There are 3 answers

0
Yilmaz On

if your question is related to setting up a database connection similar to express.js we cannot do this in next.js. I explained it here why. In Express.js, database connections are typically long-lived, meaning they are established when the application starts and remain active throughout the lifetime of the application. But in next.js due to the nature of serverless functions, we have to implement per-request connection.

But if you want to implement dependency injection, you can try tsyringe developed by Microsoft. To apply this in next.js, you can follow this. In case link gets broken in the future:

// reflect metadata allows you to use decoraters
npm install --save tsyringe reflect-metadata

Modify your tsconfig.json file to include the following settings, which will allow the use of decorators in TypeScript.

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Add a .bablerc file to your project with the following settings.

{
  "presets": ["next/babel"],
  "plugins": [ 
    # those implements the decorators
    "babel-plugin-transform-typescript-metadata",
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": true }]
  ]
}

Finally add this import to your _app.tsx file or in app directory to layout.

 import "reflect-metadata";

We're now ready to convert the code I had before into something more maintainable.

Example code creates a graphql client:

mport { DocumentNode } from "graphql";

export interface graphClient {
  query(query: DocumentNode, variables?: {}): any;
}

note that you need to use class components to inject into constuctor

import { inject, injectable } from "tsyringe";

@injectable()
export class getLatestPosts implements iGetLatestPosts {
  graphClient: graphClient;

  constructor(@inject("graphClient") private graphClientParam: graphClient) {
    this.graphClient = graphClientParam;
  }
...
}
0
Tyson Boring On

you can create a dataAccess.ts file like this:

// dataAccess.ts

import {
  MongoClient,
  Db,
  Collection
} from 'mongodb';

let client: MongoClient;

export async function connectDatabase() {
  if (!client) {
    client = new MongoClient(process.env.MONGODB_URI);
    await client.connect();
  }
  return client.db();
}

export function getSurveysCollection(db: Db): Collection {
  return db.collection('survey-templates');
}

Use Dependency Injection: To inject the database connection into your API routes, you can use a simple dependency injection approach. Create a custom middleware that handles the database connection and passes it to your route handler. Here's an example:

// middlewareDB.ts or something

import {
  NextApiRequest,
  NextApiResponse
} from 'next';
import {
  connectDatabase
} from './dataAccess';

export default function withDB(handler: (db: Db) => (req: NextApiRequest, res: NextApiResponse) => Promise < void > ) {
  return async(req: NextApiRequest, res: NextApiResponse) => {
    const db = await connectDatabase();
    await handler(db)(req, res);
  };
}

Modify Your API Routes: Now, you can modify your API routes to use the withDB middleware and the DAL functions. Here's an example of how your POST route might look:

// api/survey.ts

import {
  NextApiRequest,
  NextApiResponse
} from 'next';
import withDB from '../../middleware/withDB';
import {
  getSurveysCollection
} from '../../dataAccess';

const handler = async(db: Db) => async(req: NextApiRequest, res: NextApiResponse) => {
  try {
    const collection = getSurveysCollection(db);
    // Perform database operations using the collection
    // ...
    res.status(201).json({
      "survey-id": uuid
    });
  } catch (error) {
    res.status(500).json({
      error: 'Internal Server Error'
    });
  }
};

export default withDB(handler);

With this setup, your API routes remain clean and focused on handling HTTP requests, while the data access logic is abstracted away into the DAL. If you need to change your database backend in the future, you can make the adjustments in the DAL without affecting your API routes.

0
user3527282 On

o separate the data layer from your Next.js API routes and make your code more modular and database-agnostic, you can create a data access layer that abstracts away the database-specific code. Here's how you can achieve this:

Create a Data Access Layer:

Start by creating a new directory in your project for the data access layer. You can call it something like data-access or any name that makes sense to you.

/data-access
  - surveys.ts

In the surveys.ts file (or other files as needed), define functions that encapsulate database operations for surveys. These functions should be written in a way that they don't depend on any specific database implementation.

// data-access/surveys.ts

import { Db, ObjectId } from 'mongodb';

export interface Survey {
  // Define your survey schema here
}

export class SurveyDataAccess {
  private readonly db: Db;

  constructor(db: Db) {
    this.db = db;
  }

  async createSurvey(survey: Survey): Promise<string> {
    const result = await this.db.collection('survey-templates').insertOne(survey);
    return result.insertedId.toString();
  }

  async getSurveyById(id: string): Promise<Survey | null> {
    const survey = await this.db.collection('survey-templates').findOne({ _id: new ObjectId(id) });
    return survey;
  }

  // Add other data access methods as needed
}

Inject Database Dependency:

In your Next.js API routes, inject the database dependency by creating an instance of SurveyDataAccess and passing the database connection as a parameter to the constructor.

// api/surveys.ts

import { NextApiResponse } from 'next';
import { SurveyDataAccess } from '../data-access/surveys';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const client = await clientPromise; // Assuming clientPromise is your database connection
  const surveyDataAccess = new SurveyDataAccess(client.db('survey-db'));

  if (req.method === 'POST') {
    // Use surveyDataAccess to create a new survey
    // ...
  } else if (req.method === 'GET') {
    // Use surveyDataAccess to retrieve surveys
    // ...
  }
}

Abstract Away Database-Specific Code:

In your API routes, use the surveyDataAccess object to interact with the database instead of directly calling MongoDB-specific code. This abstraction allows you to switch to a different database in the future without changing your API routes.

Testing and Maintenance:

With this separation, you can easily write unit tests for your data access layer independently of your API routes. If you decide to switch to a different database in the future, you only need to update the SurveyDataAccess class and keep your API routes unchanged.

By following this approach, you achieve a separation of concerns and make your code more maintainable and adaptable to future changes in the database layer.