How to structure monorepo for React SSR and API

Viewed 1295

I'm trying to understand best practices of structuring monorepos for NodeJS/React projects.

I need:

  1. React SPA with server side rendering.

  2. Backend with Express, MongoDB, mongoose modules etc.

  3. Shared (common) module for some utility functions, helpers, Enums etc;

What is the elegant way to structure such architecture with monorepo (I'm going to use Lerna)?

  • Should I separate API and SSR EXpress servers?
  • If yes, Should I separate React frontend code from SSR server?
  • How to configure webpack dev environment, so other modules during development will always get transpiled (Babel) version of 'shared' module (this question is a bit out of scope, just may be solution is simple)

I understand that answer is mostly "it depends", but I need universal approach, that would work with many cases, even if it will be a bit overengineered.

Thanks

1 Answers

Well, yes, mostly it depends :). But, I just went through this transition with my own codebase so hopefully I can be of some help. I recently converted RMWC from one mono repo to multiple packages using lerna. https://jamesmfriedman.github.io/rmwc/

My answer will make the following assumptions:

  • You are releasing your packages individually (hence lerna)
  • Because of this your individual packages are well componentized and have good separation of concerns
  • You are releasing your packages under a scoped organization on npm (mine is @rmwc). This isn't required, but this particular setup let me pull off some fun webpack hacks. It is also what you would have if your company is using a private npm.

Have a Base / Core / or Utils module

Reference: https://github.com/jamesmfriedman/rmwc/tree/master/src/base

This is a single module full of common utils that all or most of your other modules will depend on. General rule of thumb, if more than one package needs it, pull it out into your base module so it can be properly versioned and shared. This will also prevent future fragmentation of your code since when you update the base module code, you will be required to refactor the components using it.

Yes, separate your SSR code

This is just good separation of concerns. If you think about it, React is isomorphic, so your components don't really care about where they are being rendered. Without having more detail on your codebase or problem you're solving, my gut would tell me to just make an additional SSR / Server package. This also makes sense from a dependency perspective. None of your components will rely on these packages, but those packages will rely on all of your components.

Part of deciding package separation also has to do with what might change frequently and infrequently. Again, highly dependent on your problem, but your server or ssr package might not change all that often if they are just responsible for rendering and hydrating the components.

I don't have a 1:1 reference for this, but the closes thing in my project is probably the root rmwc module that re-imports all of my other components. https://github.com/jamesmfriedman/rmwc/tree/master/src/rmwc

Webpack magic

Lerna contains a method called "bootstrap" that magically installs all of your dependencies and symlinks a bunch of directories together. If you're using Typescript or Flow, symlinks can cause issues or not work at all. For me, I created a path alias as a workaround.

https://github.com/jamesmfriedman/rmwc/blob/master/config-overrides.js#L38

Basically, anytime @rmwc (my npm scope) is referenced, I redirect webpack to my src folder. This is where I benefit from having them all under the same scope. If there were different scopes or package names, you could still pull off the following trick, just specifying multiple aliases.

Other issues I ran into

  • Releasing! This was not straight forward. Each package has its own dist file that had to be deployed (built es5 code). The solution, run lerna version instead of lerna release. This makes it so lerna doesn't run the npm publish step. Then you can have a custom publish script in each of your packages. https://github.com/jamesmfriedman/rmwc/blob/master/config-overrides.js#L38

  • Versioning: don't do independent versions. I only have anecdotal reasoning, but it made things more confusing and harder to track the evolution of your package. After all, even though it's being released individually, it is still a single project so it makes sense to see the version numbers move together.

  • Keep your main package.json file in the root smart with all of your scripts, keep the other packages as minimal as possible. With the size of my project, any change I make is multiplied by 38 components.

Happy hunting, hope this helps!

Related