Introduction
Recently, I released a full-stack restaurant reservation web app built using React Router in framework mode. In this post, I’ll introduce the architecture with a focus on the backend setup.
🔗 Live Demo: https://my-booking.tech/
📂 GitHub Repo: https://github.com/YuichiNabeshima/my-booking
Overall Goals for the App
In modern web applications, which require rich user experiences and interactivity, adopting a JavaScript framework for the frontend has become the de facto standard.
Commonly, frontend meta-frameworks (such as Next.js, Nuxt, Remix) are used along with backend frameworks like Express.js, NestJS, or Laravel. Alternatively, serverless backend setups (e.g., AWS Lambda) are also popular.
For this web app, I wanted to find a middle ground between simplicity and scalability, ensuring that the architecture could handle even large-scale development while still being fast and easy to implement.
I focused on monolithic architectures like Rails and Laravel, which are still widely used in many development environments. These architectures follow an MVC-like structure and have a class-based design. Inspired by this, I explored how to implement a similar architecture in a React-based framework that would be both simple and scalable.
Why React Router?
I chose React Router for this project because it was integrated with Remix last year, carrying forward Remix's philosophy of a web-standard SSR-focused framework. I thought it was a great fit for returning to a more traditional architecture.
React Router also offers flexible directory structures, which made it easier to achieve the goals of this project.
Treating the MVC Controller
In React Router, you can write loader
functions (responsible for fetching data) and action
functions (responsible for updating data) in route files (e.g., route.tsx
).
These two server-side functions can be treated as GET and POST requests, with the route.tsx
itself acting as the controller in an MVC architecture. Since route.tsx
is the entry point, it doesn't include the detailed implementation of the UI but instead imports and returns the base Page
component, which serves as the foundation for the UI components.
// routes/top/route.tsx
export function meta() {
return [
{ title: 'Top Page' },
{ name: 'description', content: 'Main landing page' },
];
}
export async function loader() {
// Fetch server-side data
}
export async function action() {
// Handle form submissions
}
export default function Route() {
return <Page />;
}
The directory structure is introduced in this article as well:
How I organized my full-stack React Router project
Architecture Diagram
The following diagram shows how data flows from the UI to the database and back, using React Router’s loader and action functions as entry points to the backend logic.
To illustrate how this MVC-like architecture is implemented within a single repository, here is a directory-level view of the codebase.
This shows how the UI, controller logic, business logic, and data access layers are all clearly separated, yet co-located in a monorepo-style structure.
Explanation of Each Layer
UI Layer
The frontend is designed in a simple React way, with a Page
component at the root, from which the components are built in a tree structure.
As shown in the diagram, using useLoaderData()
allows components beneath Page.tsx
to access pre-fetched server-side data, without needing prop-drilling or a global state management system.
Since useLoaderData()
references cached data that has already been fetched, there’s minimal performance overhead.
Controller Layer (route.tsx)
This layer acts as a mediator between the UI layer (pages) and the service layer, responsible for input processing and control logic:
- Extracting data sent from the UI layer
- Performing general validations such as required field checks and email format validation
- Calling the service layer to execute processing with the provided data
- Handling errors based on the returned values from the service layer
- Returning the result to the UI layer if no errors occur.
These are the main responsibilities of the controller layer.
Service Layer
The service layer contains the actual business logic.
In most cases, a main service class (e.g., LoaderService
or ActionService
) is called from route.tsx
. Within the service class, other repository layers or service classes are combined to handle the business logic.
For example, loader
calls the LoaderService
, and action
calls the ActionService
.
Repository Layer
The repository layer wraps ORM, such as Prisma, and is called by the service layer.
The advantages of wrapping Prisma in classes instead of using it directly are:
- It makes it easier to switch to another ORM in the future.
- Declarative methods specific to the use case can be written in the wrapper class (e.g.,
getPublishedArticle()
). - The class can be easily mocked, making it simpler to test. By using this structure, the service layer can focus on "what to fetch" rather than being coupled to a specific DB implementation, improving maintainability and testability.
Conclusion
This architecture allows me to maintain a modern React UI experience while ensuring a simple, scalable, and robust backend setup.
I hope to dive deeper into topics such as error handling and testing strategies in a future post.
Thank you for reading!
As I am currently exploring new career opportunities in the Vancouver area, If you're looking for someone passionate about modern full-stack development and architecture, I’d love to connect and see how I could contribute to your team. Feel free to contact me if you're looking for a developer with a strong passion for web development!
Top comments (1)
Most of the applications we find out there could be simple MVC, but devs like to complicate things.