Hey, Emmanuel here. While I was pondering what to write about this week, I ran into an old project of mine: Relay (A “real-time” messaging platform). Think Facebook, but without any of the stuff that makes it good, like real people and the ability to actually send friend requests. Truth be told, it’s just a badly designed message board with a fixed amount of people :( that I built to learn about Websockets.
The main thing is that I remembered the absolute nightmare it was to deploy it. It’s not that I haven’t deployed a full-stack application before. No, it’s because the “genius” that made the tutorial decided to build the frontend and backend separately and then eventually move it to the same codebase for a single deployment. He configured a single build command for both ends and a lot of shoddy fixes to get it to work.
Thanks to the experience (PTSD) I got from that project, future deployments became substantially easier. So, that gave me the idea to write about taking your app to production. After building it on your machine, what comes next?
It Works on My Machine. What's the Problem?
Having an app that works on your machine but breaks when it hits production is one of the major rites of passage for developers. When you run your app from your machine, it basically acts as a personal server to handle requests and responses. But that works for just your machine, as someone else making a request to http://localhost:5000 sends it to their own device, not yours.
So, deployment means getting your app out of your device and making it accessible to others. However, a lot, and I mean, a lot of things can easily make things spiral out of control.
While developing locally, you often know exactly where everything is, how it connects, and which configuration values it needs. Deployment forces those assumptions to become explicit. Where does the frontend run? Where does the backend run? How do they communicate? Where is the database hosted? How does the application know which URLs and credentials to use? Those are all questions you need to consider when getting your app out of development and into production.
Your App Needs a Home
Firstly, your app needs a home, which is a server that it runs on. Running it locally doesn’t allow external users on other devices to be able to send requests to your server (I know certain network configurations can let it do that, but by default, it doesn't). Therefore, you need a hosting provider, which is a platform that basically offers you a chunk of their infrastructure to run your application. The exact cloud hosting provider you choose depends on your use case, requirements, and budget. For Relay, however, I used Render (this is my go-to, as their free tier is quite generous).
As you probably know, not every part of your application lives in one place. Usually, you have the frontend of the app, which deals with the client-facing services like the UI, while the backend manages data, logic, and security. Now, the way you deploy your application depends on how you arrange the frontend and backend.
For combined deployment, the frontend is built into static files, and a server serves those files while receiving backend requests. This is actually how I first started web development, using EJS (Embedded JavaScript) alongside Express/Node.js to serve my frontend. With this arrangement, you deploy the whole thing as one, as the backend receives requests directly and serves the appropriate frontend file.
However, I eventually discovered that separate deployment was the popular choice. The frontend and backend are deployed as separate services, usually with different URLs. For Relay, I actually used this arrangement. The React frontend displayed the UI, and the backend provided the core logic, like authentication, messaging, and account creation.
So now, your frontend and backend are now deployed. However, if you used the separate deployment approach, they still need to be able to communicate with one another. So, how do we do that?
The Frontend and Backend Need to Find Each Other
It’s quite obvious that both ends of your application need to communicate to actually work. For that, we use an API (a set of rules and protocols that allows different software applications to communicate and share data with each other). During development, the backend API runs on localhost (something like http://localhost:5000/api/). Hence, the frontend sends requests to that url.
However, when deployed, both ends get live URLs. Back then, I didn’t know most of this, so I wondered why Relay didn’t work when deployed. After a few frustrating minutes, I discovered I was still sending requests to http://localhost:5000/api/ from the deployed frontend. I recommend using environment variables rather than hardcoding these paths, as it’ll be easier to switch between development and production URLs without changing your code.
Of course, there are many other things that can go wrong. In my own case, I also experienced a CORS (Cross-Origin Resource Sharing) error. Keep in mind that this is a security feature designed to stop malicious sites from secretly reading your data on other websites. Notably, tools like Postman or curl don’t enforce CORS, so your API might work during development but fail when it hits production.
Fortunately, CORS errors are straightforward to handle. All you need to do is configure your backend server to include specific HTTP response headers. Setting these headers lets you provide the live frontend URL, list the allowed methods, and even include custom headers. If, like me, you use Express/Node.js for the backend, you can do this by installing the cors package and setting it up.
Where Does Your Data Live?
More often than not, your application might need to access a database for full functionality. However, a database running locally on your development machine isn’t automatically accessible by your deployed application. For this, you use a cloud database service; Supabase, Neon, and AWS RDS can provide PostgreSQL databases, while MongoDB Atlas hosts MongoDB databases.
After setting up a database from any of these cloud providers, you get a connection string. You can then configure your backend to use this connection string for accessing the database. However, DON’T hardcode the connection details in your code; use environment variables to securely pass in the required database access credentials.
I actually used MongoDB Atlas (the tutorial made me do that :( ) while building Relay, so the connection URI was set right from the development stage.
From Localhost to a Live Application
I’ve always heard about deployment issues from other devs, but getting a firsthand experience is something else. Turns out getting an application working locally is a far different game from making it accessible, secure, and reliable once it’s live. However, as I’ve now learned, each frustrating deployment teaches a valuable lesson.
Looking back, Relay wasn't exactly my finest work, but the deployment experience taught me things I wouldn't have learned by simply following a tutorial. Take your time, understand how the different pieces fit together, and don't be afraid to troubleshoot.
