Full Stack Dev Notes MERN
This post summarizes my findings from working on the AssayPlate web app.
Outdated/Buggy Tutorials
This section is for developers new to the MERN stack.
First of all, do NOT follow MongoDB’s tutorial on MERN stack – it’s outdated in many ways. Some consequences of following this tutorial can be seen here and here.
This FreeCodeCamp tutorial has a good skeleton to build your app from, but it’s also riddled with bugs:
- CORS and
react-cookies
bugs (details here) ToastContainer
needs to be moved like this, lest it uses the wrong CSS- it didn’t tell the readers to specify
TOKEN_KEY
- it incorrectly uses
bcryptjs
where it needsbcrypt
Another note: create-react-app
is deprecated; use vite
instead!
Where to deploy the backend?
There are numerous free hosts for the static frontend; the real problem lies in where to deploy the backend.
Render is probably the best free-forever solution, although it does make your app sleep after a few minutes of idling – waking up can easily take 40 seconds. I have not tested whether it support stateful apps (AssayPlate is stateless).
Adaptable.io not only makes your app sleep upon idling, it does NOT support stateful interactions (e.g., real-time connections). For example, it cannot functionally host the Resistance Online server.
Warning against Fly.io
I had a very negative experience with Fly.io
, and I would like to warn others who are exploring this option for hosting their backend.
Fly.io supposedly offers a one-time, free $5 credit for new users, but this was not my experience. It was actually a bit confusing – suspiciously so. When I registered with my GitHub account, I was prompted to add my credit card before I was allowed to deploy an app, and once I did, I immediately got auto-subscribed to their paid Hobby plan. The whole process made it look like I was just adding a credit card for verification, NOT for subscribing to their paid plan. In fact, someone else had the same experience around the time I “accidentally” subscribed to their paid plan and made a post requesting refund that has still not gotten a public admin response.
That wasn’t the only suspicious practice. A few days after hosting my backend on Fly.io, I started seeing charges for Additional RAM. After digging around, I found out it was because, even though I specified otherwise through their configuration web app, I ended up using the default configuration that uses more RAM than the free allowance. Again, I’m not the only one complaining about this. In fact, you see posts about unexpected charges every so often on their community forum; if anything, this implies their lack of willingness to make the billing process more intuitive/transparent.
The Cookie Saga
I had a LOT of trouble with cookies when working on this project (browser not saving cookie, script not seeing the cookies visible to browser, etc.). Here is a list of things I learned, trying to resolve cookie-related issues for AssayPlate.
how to debug cookie-related issues
Use the browser’s Inspect tool and monitor the network exchanges. On Chrome, this would be the Network
tab. Check out the cookie received in each response – the browser will include hints for why certain cookies are discarded. To check what cookies are visible to the browser, go to the Application
tab, and find “Storage->Cookies”. Note: cookies that are visible to the browser may not be visible to your JavaScript code! See next section for an example.
different domains?
Initially, I hosted the backend at https://assayplate-backend.onrender.com
, which I incorrectly assumed to be on the same domain as https://assayplate.onrender.com
. This is because onrender.com
is a public suffix (see here for the entire list of public suffixes), so those two are intentionally treated as different domains.
Until I learned about the concept of public suffixes, I was configuring CORS and cookies as if the production backend and frontend were deployed on the same domain, which caused a great deal of bugs.
Now, knowing that the backend is deployed on a different domain from the frontend, we have different issues. As noted here, “Cross-domain cookies cannot be accessed. In case you are building a single page application and your server is on a different domain. You cannot access the cookies on your SPA. The withCredentials flag will just make sure that the network calls made include the cookies and accept any incoming cookies.”
For example, react-cookies
would be useless if you want to handle cookies coming from a different domain. A heisenbug that appeared during my development was how, react-cookies
was correctly handling cookies when in development, but not during production – turns out it’s because earlier in the local development, both the backend and frontend were hosted on localhost
, i.e., the same domain, but in production, they were hosted on different domains, as noted above.
FINALLY, as noted in the MDN, “Browsers block frontend JavaScript code from accessing the Set-Cookie
header, as required by the Fetch spec, which defines Set-Cookie
as a forbidden response-header name that must be filtered out from any response exposed to frontend code.” As a result, the frontend script cannot directly parse the Set-Cookie
header, either.
In the end, I had to rely on the browser to handle the cookie exchanges, since my script cannot access cross-domain cookies through react-cookies
nor parse the cookie headers directly. E.g., I had to make the frontend ask the backend to invalidate the cookie rather than having the frontend script delete the cookie locally.
cookie configuration rules
there are some very important cookie configuration rules that, when violated, can lead to cookie-related bugs. Check loginCookieOptions
here for the correct cookie configurations for AssayPlate.
- cookie configuration must be done correctly otherwise browser will reject the cookie (e.g., claiming that
sameSite
istrue
while sending a cookie from a different domain). - setting
httpOnly
totrue
will make the cookie inaccessible to the JavaScriptDocument.cookie
API, but this only matters when dealing withsameSite
cookies, since the script cannot access cross-domain cookies regardless (see previous section) - Google is restricting third-party cookies! This will soon break some cookie uses that don’t follow their new standard. Attempting to follow the new standard, I tried to set the
partitioned
flag in the cookie configuration, and ended up uncovering a bug withexpress.js
where the flag couldn’t be set correctly (see here).
react-cookies quirks
even when we can use react-cookies
, there are still some things to pay attention to, to avoid bugs:
- use the
CookieProvider
wrapper! AND set the correctdefaultSetOptions
, especially the cookie path. - call
useCookies
correctly… this is more of a JavaScript tip, though. Don’t be dumb like me.
CORS learning
CORS is about letting the browser allow a script from site A to make requests to site B (site B has to define its CORS policy to allow requests from site A). The point is, the browser usually sends the user’s cookies for site B to site B whenever the browser makes a request to site B. If the request was a malicious request NOT made on the user’s behalf, but rather on site A’s behalf, then there is a problem – the cookies can contain authentication information; site A wouldn’t have been able to make requests to site B on its own; site A needed user’s cookies to access site B.
As a result, a number of APIs (e.g., fetch()
and XMLHttpRequest
) follow same-origin-policy
– if the request for content is not by a script from the same origin (site B script requesting from site B), then the request is denied in the preflight check.
Note, again, it’s the browser and the server together that enforces this. When you manually make HTTP requests, you can choose to ignore CORS, fake your origin, etc, although this fact doesn’t help malicious parties that want to coax the browser into making illegitimate CORS requests.
Tips
This section has some tips for good practices in full-stack development.
HTTP tips
GET requests (or DELETE, etc.) shouldn’t have a body:
https://www.baeldung.com/http-get-with-body. Just use POST
most of the time and structure the actual URLs in terms of CRUD (see this project’s API Documentation)
MongoDB/Mongoose tips
Mongoose documentation can be confusing/lacking at times. Examples:
“Note: Like updateOne, Mongoose registers deleteOne middleware on Query.prototype.deleteOne by default. That means that Model.deleteOne() will trigger deleteOne hooks, and this will refer to a query. However, doc.deleteOne() does not fire deleteOne query middleware for legacy reasons. To register deleteOne middleware as document middleware, use schema.pre(‘deleteOne’, { document: true, query: false }).” Reference here.
I coded a “smart” way to remove dead references upon population from document. It’s more efficient than the answer here, inspired by this downvoted answer in the same post, but does require marking as modified. See GetPlatesAndUsername
in PlatesController.js here
react.js tips
- to share states between components, lift them up. For complex state management, use dedicated libraries like Redux.
- state variables should be treated as immutable. Not following this practice easily leads to bugs.
node.js tips
The canonical way to check/set whether the code is running in production is via the NODE_ENV
environment variable, as noted here
JavaScript tips
- Empty arrays and objects are truthy – be careful when using comparison shorthands!
- order matters when destructuring an array (as opposed to an object)! Here’s one way it led to a pretty annoying bug, complicated by other issues with cookies mentioned in the earlier section
Axios tips
Axios interceptor doesn’t natively support React hooks. If you really want to use it (e.g., to check all responses for a token expiration flag so you can log the user out), you can try following this workaround. For the record, I hate such workarounds – they tend to be inefficient and result in tech debt.
react-draggable tips
react-draggable
currently behaves inconsistently on mobile for conditional renders. See my GitHub issue here.