- Published on
Mise - Case Study
- Authors
- Name
- Oscar Mejia Bautista
The Challenge:
Candelari's and its businesses have a vital mission: To deliver the authentic Italian experience to people in Houston. An efficient management of their inventory and orders needs to occur in order for them to be able to accomplish that. Candelari's and its businesses needed a comprehensive technology program to improve their inventory management, employee training, communication between managers and kitchens, and orders between the kitchen commissary and its locations. As an expanding businesses, critical improvements were identified to be able to accommodate growing pains.
Disclaimer:
The app is behind a log in wall. Hence, I cannot link to it publicly. But see the documentation I wrote for a detailed breakdown of the app:
Mise Documentation For Kitchen Managers
Deciding my Tech Stack
Candelari's and its businesses are expanding so their needs are in constant flux as they are in the process of understanding what works for them at a larger scale. Hence, the tech stack needed to be nimble in order to be adaptable to the changes of a growing business, and be reliable so the user gets a professional experience. Thus, I built Mise to be an app that brought those qualities together.
Database
When deciding a database I had two considerations. What type of database I was going to use, and where I wanted to host it.
- Choosing a Database:
SQL
- Pros 👍🏼:
- Given that the App was going to be heavily relational in nature. The natural choice was SQL.
- SQL is type safe. Hence, increases the reliability
- Prisma db push allows me to skip migrations if I really want to. Thus, I can be nimble like with noSQL but keep retain the type strictness and relational benefits from SQL databases bring.
- Pros 👍🏼:
NoSQL
- Pros 👍🏼:
- I don't have to define a schema. Thus, I can stay nimble.
- Cons 👎🏼:
- Not having a schema, often leads to less reliable data.
- Less reliable data leads to more bugs, more bugs lead time burn(lose speed).
- Pros 👍🏼:
Choose a Cloud Provider (Heroku, Railway, Render, PlanetScale).
I considered PlanetScale due to their superb version control database management features, however, given the scale of the project, I felt it was overkill. If the project had an unlimited budget, I would have gone with PlanetScale.
Railway was the easiest to set up and their payment costs were right. I spun up a Postgres staging and production instance in less than 5 minutes 😳
In the end, I went with a PostgreSQL on Railway as it satisfied the reliable and nimble qualities.
My database schema went on to look like the following:
Backend
For the backend, I was primarily looking for a safe way to call my backend from my frontend. This would form increase the reliability of the data going into my frontend since it would be all typed and my components would know what to expect. Nevertheless, also nimble since it would allows me the flexibility to manuever in the frontend without having to worry about breaking anything. Again, all the data flowing in my frontend is typed, therefore I know data goes into what.
REST, GraphQL, and tRPC were all ok candidates, however, tRPC was the the best for this project. Here are some of the trade offs I considered:
GraphQL
- Pros 👍🏼:
- Creates a type safe way to call my backend from the frontend (Ex: GraphQL Code Generator).
- Useful for establishing a contract between the Frontend and the Backend.
- Suited for a public facing API.
- Solves over-fetching in the front end (ex: Performance issues).
- Can use multiple programing languages.
- Cons 👎🏼:
- Its mental model takes more work to plan.
- Relies on Code Generation to safely call backend from the frontend.
- Migrating off it is more complex.
REST
- Pros 👍🏼:
- Creates a type safe way to call my backend from my frontend (Ex: Swagger)
- Can use multiple programing languages.
- Suited for a public facing API.
- Easier mental model than GraphQL. I just create end points as I need them.
- Migrating off it is simple.
- Cons 👎🏼:
- Relies on Code Generation to safely call backend from the frontend.
tRPC
- Pros 👍🏼:
- Creates a type safe way to call my backend from my frontend.
- Automatic typescript connection between the front end and the backend. For example, if I make a change in the backend, typescript intellisense will document it live on VSCode.
- Intellisense auto documentation. Hence, I open up some curly braces and I get a list of the apis available in my backend.
- Easiest mental model. I just create end points as I need them, and get intellisense automatically.
- Migrating off it is simple. I just copy paste my code into resolvers and endpoints.
- Cons 👎🏼:
- Has to be a monorepo.
- Tight coupling of frontend and backend.
- Backend has to be written in typescript.
- Best suited for internal services.
Overall, tRPC had the best trade offs for Mise application. The cons were all negligible for my case, and I got to reap all the pros. Alas, I had a safe way to call my backend from my frontend, I get top tier automatic intellisense, and if I ever choose to migrate off it, the process would be easy. Thus, the nimble, and reliable qualities I'm looking for were upheld.
Next.js vs Custom Backend
Given that tRPC made the most sense for me, I embraced the coupling of frontend and backend, and went with Next.js API routes as the backend. It worked amazing. I got a fullstack monorepo out the box, with fullstack typescript, minimal set up, and the freedom to render on the server or on the client as needed.
Auth0
In the spirit of focusing my time in actually building the application, I went with Auth0. I had everything having to do with authentication set up in less than hour. And most of the features around auth0 were well documented and easy to grasps. Hence, going with Auth0 was an excellent choice for my application. This satisfied the nimble constraint too since I could easily build my own auth without sacrificing previous work because setting up auth0 in next.js is no more than a few lines of code.
I considered NextAuth.js, however for this application having a classic credentials(email and password) authentication was important, and this authentication method is explicity marked as limited in functionality withNextAuth.js. Thus, I refrained from NextAuth.js for this specific case.
Prisma
I picked Prisma over TypeORM and Sequelize because prisma on average has better tooling, better documentation, and the prisma schema language is easier to reason about than expressing the database shape using classes.
As far as tooling for prisma goes. I love prisma studio. Having instant access to run CRUD operations in my database through a GUI allowed me to experiment on the fly multiple times through development.
Prisma for 90% of use cases Prisma does the job very well; for the other 10% you can run raw SQL queries.
ZoD
ZoD is simply a validation library. Thus, it helps validate inputs for query and mutations and it takes care of typing the input too. Even though what it does is so simple, it deserves a shout out because it does it so well it well feels magic 🪄. The right kind 🦄.
For example, in my itemsByRestaurant
query:
We define the input object with ZoD
export const itemsByRestaurantSchema = z.object({
restaurantId: z.string(),
})
Place our value in the input for the prop for my itemsByRestaurant
query
.query("itemsByRestaurant", {
input: itemsByRestaurantSchema,
async resolve({ ctx, input }) {
const itemsByrestaurant = await ctx.prisma.item.findMany({
where: {
restaurantId: input.restaurantId,
},
orderBy: {
name: "asc",
},
include: {
unit: {
orderBy: {
name: "asc",
},
},
},
});
return itemsByrestaurant;
},
})
And boom, now my query is secure against incorrect inputs from my frontend, and the input variable in the resolver is typed.
If I send the wrong input the server, typescript will not let me. Points for reliability ✅
Backend In Practice
In practice, putting all these technologies together in Mise looked like the following endpoint:
Prisma having everything typed off the box 🚀
ZoD typing my inputs 😎
And tRPC bringing it all together seamlessly 🫡
Now this is the true power of typescript! 💥
Frontend
For this App I had the freedom to either render it on the client, the backend, or a mix of both since the app was all behind a credentials log in. I opted to client render most of the app in order to have it feel snappy and interactive to the user. I used React Query to manage the server state, React Hook Form for managing state in forms, useState for simple UI state, and useReducer for more complex state.
React Query
React Query is an amazing library to handle server state. With React Query, when data changes, there are usually three strategies one can use:
Invalidate queries whenever serer data was updated
- Pros:
- Less Code to Write.
- Cons:
- Potential Performance Issues.
- Doesn't show instantly in the UI.
- Pros:
Update cache with mutation response
- Pros:
- More Code to Write.
- Cons:
- Doesn't show instantly in the UI.
- Pros:
Optimistic queries
- Pros:
- Way to write code WAY more code to handle update cache manually, and rollback in case of error.
- Display instantly in th UI. Which makes for a snappy and fluid user experience.
- Cons:
- Server request might fail, and have to rollback. Which may lead to flashy/weird looking behavior in the UI.
- Pros:
For Mise, I mostly stuck with strategies 1 and 3. Most queries in Mise are light enough to not cause performance issues if re-fetched. Furthermore, writing less code is less room for error which helps my reliability condition.
React Hook Form
I used React Hook Form for most of the forms. As Forms get large, and/or complex in React, there are often performance issues. Hence, React Hook Form handles many of the Forms in Mise.
There are potentially 100+ inputs in this form, it would be inefficient to have to render the entire form every time an input changes. The performance loss would be palpable 🐢. Hence, a good candidates for react hook form.
useReducer
The vast bulk of the state in Mise is server state, hence it is handled by React Query, and form state is handled by react hook form. However, I needed to consider an additional form of state management solution for a feature in Mise. Mise has a checkout feature for Kitchen Managers. Kitchen managers fill out an order, and if the order is correct, they submit it.
Here are pictures of what it looks like:
The challenge here was to collect user information through different pages and form a cohesive way of representing that through state. My state needed to encapsulate the following:
- The state from the form when the user finishes selecting items to order
- State from what step in the order flow the user is. Example: Ordering, Order Confirmation, and Order Success.
- The Id number of the order once created.
A flux like store would fit this use case the best because it would encapsulate the logic in one place, and it would allow us to handle state updates by named events. Here is a mental picture, to get a better idea:
The classic useState
approach would leave us with state updates being handled across multiple components and I would not be able to label the event updates with a name. For these reasons, I opted for a flux like store state management solution like useReducer to handle the state here.
React has too many state management solutions, that said, I like having a vast number of libraries to choose from. It feels like I am choosing from a toolbox where each tools is specialized for a job.
Libraries like Jotai, and Recoil looked cool, but rather than provider a flux(store, actions, and reducer) like pattern, they provide better primitives state primitives. Hence, I felt like they didn't fit my use case the best. In my case, I needed to build something that would encapsulate the logic in a store.
useReducer
in this case is representing the bulk of Flux like state management solutions like Redux and Zustand. That is not to say that each of these state management libraries do not have trade offs of their own. For instance, Zustand would without a doubt have better performance than useReducer + Context, but given that the user will only submit orders a couple of times a day -- there won't be a performance problem. Hence, the tradeoffs are negligible in my use case.
Finally, I narrowed down my options down to useReducer and XState.
XState is different than the Flux like solutions. XState goes a step further. XState's model constraints the user to describe every possible combination of the state. This in turn creates a state machine. This is easier explained through a picture:
This adds additional safeguards because events can not happen in any arbitrary order. Thus, you can only go from specific states to other states. useReducer just gives you the generic ability to change to any arbitrary state. Hence, there is room for states that should never happen to happen via human error. Furthermore all of this logic is in one place, we liked how flux centralized all of your logic into one place. StateX does that in a comprehensive manner.
Weighting the Pros and Cons looked like the following:
useReducer
- Pros:
- Less learning curve
- Cons:
- Doesn't scale well
- Less reliable
- Logic is mostly in one place
- Pros:
stateX
- Pros:
- Scales well
- More reliable
- All the logic is truly in one place
- Cons:
- Higher learning curve.
- Pros:
Ultimately, I stuck with useReducer even though XState is technically better for my scenario. I found XState had a high learning curve. I did not feel confident in getting XState working during my time constraint. If I had more time, I would had gone with XState.
UI
For the UI I considered between Chakra UI, tailwindcss, and Ant Design.
Ant Design is an opinionated UI React framework. It has an opinion on how it is going to look, so it less customizable, and it has opinions on how to handle data submitted from forms and such. Given that I wanted to use React Hook Form to handle the state in my forms -- hacking it to work with AntD is possible, but it would require more effort. Thus, I refrained from using AntD.
Tailwind has mostly everything I'm looking for. At its core it is just css so it is minimally intrusive and won't conflict with how I may want to manage state or my forms. Furthermore, it is just css so it framework agnostic if I ever need to switch off react. Furthermore, it is highly customizable to whatever look I want to give it.
Chakra UI is a less opinionated UI React framework that is just at the right sweet spot of doing things for you and giving you the freedom to handle things how you see it. Furthermore, it is React out the box. Hence, I can just copy paste Chackra UI components into my code and have a working example instantly.
Having taking all these considerations into account. I felt that Chackra UI slightly edged Tailwind for my specific use case, and that I was going get the most productivity, and control when I needed from it. Thus, I went with Chackra UI to handle my UI.
Conclusion
Ultimately, I feel content with most of the choices I made. They were not perfect, but I am proud of having all the trade offs in mind, and making an educated decision from that place. Mise was an awesome project that it is going to help Candelari's and its businesses expand. Delicious things are in store 🍽.
Credits:
Some thoughts in this blog were inspired by the following videos:
STATE IN REACT - Redux vs React Query vs Zustand vs Jotai vs…: https://www.youtube.com/watch?v=5-1LM2NySR0&ab_channel=Theo-ping%E2%80%A4gg by Theo Stately useReducer vs StateX: https://www.youtube.com/watch?v=FrNXCJa5FLs&t=194s&ab_channel=Stately