Next.js Discord

Discord Forum

Abstraction of Ecommerce Backend

Answered
Mini Satin posted this in #help-forum
Open in Discord
Mini SatinOP
What approach could I use to abstract away the "backend" of my ecommerce app? For example, I'd like to keep most of my app uniform, but easily switch between talking to a SQL db and talking to say a Shopify API, or a Firebase firestore.

I'm thinking I could go with a Nodejs backend for everything, then each one could have different server actions. I'm new to Nextjs so not sure what strategies are available and used, and I'm trying to develop a simplish emcommerce framework, and then maybe a plugin type architecture with one plugin for Firebase, one for direct db comms, and others for different shop APIs.
Answered by B33fb0n3
Yea, in conclusion:
Either do functional programming or OOP. For OOP factory and service locator should fit that use case.

For functional programming a wrapper and this would work: https://nextjs-forum.com/post/1458147135101993084#message-1458158002501582858 with this https://nextjs-forum.com/post/1458147135101993084#message-1458149488832286863
View full answer

8 Replies

@Mini Satin What approach could I use to abstract away the "backend" of my ecommerce app? For example, I'd like to keep most of my app uniform, but easily switch between talking to a SQL db and talking to say a Shopify API, or a Firebase firestore. I'm thinking I could go with a Nodejs backend for everything, then each one could have different server actions. I'm new to Nextjs so not sure what strategies are available and used, and I'm trying to develop a simplish emcommerce framework, and then maybe a plugin type architecture with one plugin for Firebase, one for direct db comms, and others for different shop APIs.
I like to use my /api folder in my nextjs app. Then I create groups there. For example /products or /users or similar. And then I like to have an api.ts in there, that does the whole business logic. Independend of which other API is used there or if I use my own backend. And then actions.ts also exists for the server actions. For mutating data it looks like this then:

Client -> actions.ts -> api.ts.

In other words:
Client -> Controller Layer (just handler) -> Service Layer (Just business logic)

And like that you can apply whatever you want to it and still have a unifed system.

And like that each folder is fine: easy extension, but also [SOLID](https://en.wikipedia.org/wiki/SOLID)
@Mini Satin Thanks, that looks like a neat approach, and good separation of concerns, but then I'm still wondering how to cater for e.g. multiple data providers. Would you have different actions.ts files for different providers? Let's say I want to switch between a Firestore noSQL db and Primsa connected to a "local" Postgres?
If it would be the same DB type, then you can just use a different env variable and the rest stays the same. If the provider is not the same, then the functionality (like how things work) also differ.

So having a different functionality, but wanting to use the same code is impossible. But we can create wrappers, to support it (tbh I never done it, but in my mind the following would work).

/api
_firestore
_prisma
/your-folder
- api.ts
/another-folder
...

And those are just wrapper around the provider functions:
// in 'your-folder'
export const someService = {
    someServiceMethod: (input: Something) => {
        if(process.env.PROVIDER === "FIREBASE") // or other condition
          return yourFireServiceForThisService.theMethod(input)
        else
          return yourPrismaServiceForThiService.theMethod(input)
    }
}

And then you can create your methods like you expect. In both cases: you want to have clear interfaces. Else theMethod and their inputs might differ (you dont want that). And also a good typing system: reuse or you'll swim in a lot of types (you dont want that either)
@B33fb0n3 If it would be the same DB type, then you can just use a different env variable and the rest stays the same. If the provider is *not* the same, then the functionality (like how things work) also differ. So having a different functionality, but wanting to use the same code is impossible. But we can create wrappers, to support it (tbh I never done it, but in my mind the following would work). /api _firestore _prisma /your-folder \- api.ts /another-folder ... And those are just wrapper around the provider functions: ts // in 'your-folder' export const someService = { someServiceMethod: (input: Something) => { if(process.env.PROVIDER === "FIREBASE") // or other condition return yourFireServiceForThisService.theMethod(input) else return yourPrismaServiceForThiService.theMethod(input) } } And then you can create your methods like you expect. In both cases: you want to have clear interfaces. Else `theMethod` and their inputs might differ (you dont want that). And also a good typing system: reuse or you'll swim in a lot of types (you dont want that either)
Mini SatinOP
Something along the lines of wrappers is what I'm looking for. I'm not trying to use the same code, but find a way of neatly switcing between different modules for different providers.

In ASP.NET I inject service implementations into controllers that only consume interfaces for the services, not the implementation of the services. So I can have one Product service that connects to a db, and another that calls an api instead.
@Mini Satin Something along the lines of wrappers is what I'm looking for. I'm not trying to use the same code, but find a way of neatly switcing between different modules for different providers. In ASP.NET I inject service implementations into controllers that only consume interfaces for the services, not the implementation of the services. So I can have one Product service that connects to a db, and another that calls an api instead.
yes you can do this here too. My approach was more a like functional based approach. You can do the same OO. yourFireServiceForThisService would be then a class that implements ThisService and then it receives all methods that are defined there:

interface ThisService {
  getSomething(): Promise<boolean>;
  listSomething(): string[];
  updateSomething(data: string): void;
}

class yourFireServiceForThisService implements ThisService {
  async getSomething(): Promise<boolean> {
    console.log("Service getSomething");
    return true;
  }
  listSomething(data: string): string[] {
    return [`Listing: ${data}`];
  }

  updateSomething(): void {
    console.log("updating something");
  }
}

class yourPrismaServiceForThiService implements ThisService {
  async getSomething(): Promise<boolean> {
    console.log("Service getSomething");
    return true;
  }
  listSomething(data: string): string[] {
    return [`Listing: ${data}`];
  }

  updateSomething(): void {
    console.log("updating something");
  }
}

const service = new yourFireServiceForThisService(); // For example
await service.initialize();
const result = service.process("test data");

But then you still need some way to decide when to use which service. ThisService is then just a group of api routes. It can be ProfileService or UserService or whatever
Even with an correct interface you would need to have a "wrapper" controller, that handles the stuff:
class YourController {
  constructor(private service: ThisService) {}

  async handleRequest() {
// The controller only has the interface
    const result = await this.service.getSomething();
    const list = this.service.listSomething();
    this.service.updateSomething("test data");
    return { result, list };
  }
}

// But you still need to provide the correct service
const fireService = new yourFireServiceForThisService();
const controller1 = new YourController(fireService);

const prismaService = new yourPrismaServiceForThiService();
const controller2 = new YourController(prismaService);

await controller1.handleRequest();

I guess you know what you doing when you are in OOP. Factory would fit as well IMO or service locator should work as well.

If you want to keep it simple:
const fireService = new yourFireServiceForThisService();
const prismaService = new yourPrismaServiceForThiService();

const usedService = process.env.NODE_ENV === 'development' ? fireService : prismaService
const controller = new YourController(usedService)

await controller.getSomething() // will work independend of the service behind it. 'controller' is the wrong word here, but you get what I mean
Yea, in conclusion:
Either do functional programming or OOP. For OOP factory and service locator should fit that use case.

For functional programming a wrapper and this would work: https://nextjs-forum.com/post/1458147135101993084#message-1458158002501582858 with this https://nextjs-forum.com/post/1458147135101993084#message-1458149488832286863
Answer