简体   繁体   中英

How to apply layered architecture of Java/Spring in NodeJs?

I have been trying to learn NodeJS for quite some time now. All the books and tutorials seems to follow similar pattern of structuring their code. Example -

const express = require('express');

const app = express();
app.set('view engine','hbs');

app.get('/', (req, res) =>{
    res.render('index');
});

app.get('/getName', (req, res) =>{
    // Mock DB call to fetch Name
    res.render('displayName');
});


app.listen(3000, () => {
    console.log("Started server on port : 3000");
});

As you can see above, the /getName controller is performing DB call as well as returning the view. So the business logic as well as the CRUD operation is being done at the same place.

I come from the world of JAVA and there we do it slightly differently. For example, a Spring Boot application would contain the following structure -

  • DTO
  • Repository
  • Service
  • Controller

So, controller classes are the actual endpoints which do not perform any business logic but call the underlying service class to handle all that. The service classes implement the business logic and persist/fetch data required for it with the help of the repository classes. The repository on the other hand handles the CRUD operations.

This seemed like a sane way to develop software. Given that each class has their defined roles, it becomes really easy to handle any change.

I understand the NodeJs is a dynamic but -

1. Is there a way to separate out the functionality like we do in Spring ? If not,

2. How to structure large projects having multiple endpoints and DB transactions.

Regards


EDIT -

Consider the following scenario -

I have a requirement, where I need to fetch a list of users from the database whose status is True ( assume status is a boolean field in the model ).

In Java -

@Service
public class UserService {

    @Autowired
    UserDetailRepository userDetailRepository;

    @Override
    public UserDetail getUserDetail (boolean status) {
        UserDetail userDetail  = UserDetailRepository .findUserDetailByStatus(status);
        return userDetail  ;
    }

Controller.java -

@GetMapping("/getUserDetails")
        public ArrayList<UserDetail> getUserDetail(){

        return UserService.getUserDetail(true);
}

Now, if the requirement changes and there needs to be a new endpoint that returns only top 10 user details whose status is true. In that case, we can add a new controller and just limit the returned results to 10. We can make use of the same business logic/ service class.

Controller.java

@GetMapping("/getUserDetailsTop10")
            public ArrayList<UserDetail> getUserDetail(){

            List<UserDetails> users = UserService.getUserDetail(true);
            // Filter the list to send top 10
            // return users .
    }

If I have to implement the same use case in NodeJS, I'll have to write the business logic to fetch the user twice -

const express = require('express');

const app = express();
app.set('view engine','hbs');

app.get('/getUserDetails', (req, res) =>{
    // Call DB and get users whose status is True
    res.send(userdetails);
});

app.get('/getUserDetailsTop10', (req, res) =>{
    // Call DB and get users whose status is True
    // Filter the returned results and limit the result to 10 values only
    res.send(userdetails);
});


app.listen(3000, () => {
    console.log("Started server on port : 3000");
});

At best, I can abstract away this logic into a function, that will return a list of users with status True but then again this approach is not very scalable. There has to be a complete separation of Business logic from controller.

UserService.js

import { userDetailRepository } from '../models/user';

class UserService {
    getUserDetail (status) {
        const userDetail  = UserDetailRepository.findUserDetailByStatus(status);
        return userDetail;
    }
}

export const userService = new UserService();

UserController.js

import { userService } from '../services/user';

@RouterAdapter
class UserController {
  @GetMapping("/getUserDetails")
  getUserDetail() {
    return userService.getUserDetail(true);
  }
  @GetMapping("/getUserDetailsTop10")
  getUserDetailsTop10() {
    return userService.getUserDetail(true).slice(10);
  }
}

export const userController = new UserController();

app.js

import * as express from 'express';
import { userController } from './controllers/user';

const app = express();
app.set('view engine','hbs');

app.use(userController);

app.listen(3000, () => {
    console.log("Started server on port : 3000");
});

I intentionally "warp" my js code to match with java style so perhaps you feel at home. Personally I don't write js this way, I prefer using function way more than class.

I did not include the implementation of two decorators ( @RouterAdapter, @GetMapping ) I used, that's a detail not related to the discussion. I can assure you this is doable in js. The only thing missing is that js doesn't have runtime method overload, so I have to name 2 diff controller methods.

The point I'm making here is that design patterns are mostly language agnostic. If you're familiar with the lang, you can always find a way to handle separation of concern well.

Now I intentionally use class in above example, but a function acting as a UserService doesn't make it less reusable. I don't see why you think "it doesn't scale".

An idea:

const express = require('express');

const app = express();
const { userDetails } = require('./usersBusinessLogic.js')
app.set('view engine','hbs');

app.get('/getUserDetails', async (req, res) =>{
  const limit = parseInt(req.query.limit || '0')
  if (IsNaN(limit)) {
    res.status(400)
    return res.end('limit expected to be a number')
  }
  const detailsResponse = await userDetails({ limit })
  res.send();
});

And then you write your logic as function userDetails({ limit, ...rest } = { limit: 0 }) .

In another file ie usersBusinessLogic.js :

async function userDetails({ limit, ...restOfTheOptions } = { limit: 0 }) {
  console.log({ limit, restOfTheOptions }) // TODO logic here.
  // const result = await db.find('users', {...})
  // return serialize(result)
}

// function serialize(result) {
//  return JSON.stringify(result)
// }

module.exports = {
  userDetails
}

After which you can call GET /userDetails or GET /userDetails?limit=10 .

Let me know what do you think.

Not really an answer... but some thoughts, as I come from C# and started as a NodeJs developer 3 years ago.

Dependency Injection

This is (sadly) rarely used in many NodeJs projects that I see. There are some npm modules that provide this feature, but none of them have convinced me with a proper approach and API. Dependency Injection is still perfectly possible, only, a bit uglier because of some boilerplate code you have to write. So sadly no easy @autowire approaches. So yes you can do Dependency Injection like with Spring, but not as convenient. But let my opinion not stop you from researching Dependency Injection libraries for node.

Architecture

You can still use the concepts of DTOs, Repositories, Services and Controllers. Only for some (odd) reason, the majority of tutorials and guides out there forget about common sense architecture and just throw everything within a single controller. Don't let them seduce you in forgetting about the concepts you learned in Java. Not that OOP and Java doesn't have flaws, but there is a difference in "writing JavaScript style code" and "let's forget about proper architecture all together".

Do note that you might see some more "functional programming" patterns rise in the NodeJs community, which is not bad (but neither is OOP).

How to structure large projects having multiple endpoints and DB transactions.

Ignore the bad examples you see out there, just follow your gut feeling from your Java experience how to structure your code, with the right layers, responsibilities and Design Patterns. Only keep in mind that you don't have interfaces and there is no easy alternative to that. So you'll have to learn to work your way around that, but you can still write elegant code without them.

Middleware

Middleware is a common concept for NodeJs applications, they are similary-ish to the proxy/aspect oriented programming style. You might find example projects with excessive amount of middleware, don't let yourself be tempted by that either. Middleware is good for auth/serialisation/headers/security but not for business logic. I've seen middleware hell and middleware driven development, it ain't pretty.

Repository

A lot of people use directly the MongoDb/Mongoose/SQL client without using the adapter pattern like Repository, leaking MongoDb queries all over their solution.

My advice for you would be, just use the same structure and approach you are familiar with, but with the tools that JavaScript gives you. I genuinely love JavaScript, but it makes me cringe when I look at the lack of design and architecture that I see in the mainstream resources, sure small projects and PoC's don't need extensive design, but often when project grow, they don't clean up. How you structure a project should be agnostic to the language and framework you are writing in.

Happy coding 😄

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM