Going Beyond Static Hosting: Deploying a Node.js Contact Form API on AWS EC2
Source: Dev.to
Why EC2 for a Contact Form?
To be clear, EC2 is not the “best” tool for hosting a static portfolio. Managed services exist for a reason, and they usually provide better scalability, security, and simplicity for this kind of use case.
However, EC2 is one of the best tools for learning. I wanted to understand how applications run on real servers, how backend services are deployed, how traffic flows through a system, and how uptime and reliability are handled in practice.
Specifically, I wanted hands‑on experience working with Linux servers, managing long‑running processes, configuring reverse proxies, and handling real HTTP requests. That learning goal is what made EC2 the right choice for this project.
What I Built
For this project, I built a small Node.js and Express API with two simple endpoints:
- GET
/api/health– health check - POST
/api/contact– receives contact form submissions from my portfolio
Instead of relying on third‑party form services, my frontend sends form data directly to this backend API, giving me full control over the request flow and server behavior.
High‑Level Architecture
At a high level, the architecture looks like this:
- A user interacts with my portfolio in the browser.
- The request travels over the internet to an AWS EC2 instance.
- Nginx receives the incoming traffic on port 80.
- Nginx forwards API requests (
/api/*) to the Node.js application running on port 3000 and managed by PM2.
This setup mirrors how many real‑world production systems are structured, even at much larger scales.
Step‑by‑Step Overview
- Configure the EC2 security group – SSH (port 22) restricted to my IP, HTTP (port 80) open to the world.
- Launch the EC2 instance – Ubuntu AMI, free‑tier eligible instance type.
- Connect via SSH using a key pair; this reminded me that servers are not “deploy‑and‑forget” resources.
- Install Node.js (LTS) and npm on the server.
- Copy the backend project to the EC2 instance, run
npm install, and verify the API withnode server.js. - Introduce PM2 – a process manager that runs the app in the background, restarts it on crashes, keeps it alive after SSH disconnects, and ensures it starts on reboot.
- Install Nginx and configure it as a reverse proxy: public traffic arrives on port 80, while requests to
/api/*are proxied to the Node.js app on port 3000. This step introduced real‑world request routing and required fixing a trailing‑slash issue in theproxy_passconfiguration.
# Example commands
ssh -i my-key.pem ubuntu@ec2-xx-xx-xx-xx.compute-1.amazonaws.com
sudo apt update && sudo apt install -y nodejs npm
npm install
node server.js
pm2 start server.js --name contact-api
pm2 startup
pm2 save
sudo apt install -y nginx
# Edit /etc/nginx/sites-available/default with proxy_pass configuration
sudo systemctl restart nginx
Testing End‑to‑End
Once everything was wired together, I tested the setup end‑to‑end:
- Used
curldirectly from the server. - Inspected requests in the browser’s Network tab.
- Submitted the contact form from the live portfolio.

Seeing consistent 200 OK responses from a cloud‑hosted backend felt very different from testing locally and made the entire system feel real.
What I Learned
- Backend services on cloud servers require proper process management—PM2 is essential for keeping Node.js apps alive.
- Nginx is a common front‑end proxy that handles request routing, TLS termination, and security hardening.
- Operating a service involves more than just running code; it includes security, monitoring, and reliability considerations.
- Working with Linux, AWS EC2, Node.js, and Nginx gave me confidence in production‑style workflows and reinforced why managed services exist—they hide a lot of underlying complexity.
If you’re early in your cloud journey and curious about what happens under the hood, I highly recommend trying something similar.