8 minute read

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 needs bcrypt

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.

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.

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 is true while sending a cookie from a different domain).
  • setting httpOnly to true will make the cookie inaccessible to the JavaScript Document.cookie API, but this only matters when dealing with sameSite 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 with express.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:

  1. use the CookieProvider wrapper! AND set the correct defaultSetOptions, especially the cookie path.
  2. 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

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

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.