Deploying a full stack JavaScript application sounds straightforward until subtle runtime differences surface in production. What works locally can fail in a managed environment. This article walks through the architecture and deployment of a traffic-splitting URL shortener built with Node.js, Express, React, and Render, including a key issue encountered with Express version compatibility.
Architecture Overview
The system implements the following:
- URL shortening with unique short codes
- Traffic splitting between two destination URLs
- Hit tracking per destination
- Computed total hit aggregation
- Password-protected admin dashboard
- React frontend served from the same backend service
- Deployment as a single Web Service on Render
The backend uses:
- nanoid for generating short codes
- bcrypt for secure password validation
An in-memory Map is used as a lightweight data store:
const cache = new Map();
Each short code maps to an object containing metadata, two destination URLs, hit counters, and a computed getter.
Traffic Splitting Logic
The redirect route accepts a short code and randomly selects one of two stored URLs:
const idx = Math.random() < 0.5 ? 0 : 1;
data.urls[idx].hits += 1;
res.redirect(data.urls[idx].link);
This approach enables:
- Basic A/B testing
- Lightweight traffic experimentation
- Simple load distribution scenarios
Each redirect increments the hit counter for the selected destination.
Computed Properties with Getters
Instead of storing a derived value, total hits are computed dynamically:
get totalHits() {
return this.urls.reduce((sum, url) => sum + url.hits, 0);
}
This avoids duplication and guarantees consistency between individual hit counters and aggregate metrics.
Protecting a GET Route
Although GET endpoints are often associated with public access, they can be protected using middleware. The admin dashboard requires a password passed as a query parameter and validated using bcrypt:
const protectAdmin = async (req, res, next) => {
const password = req.query.password;
if (!password) return res.status(401).send("Input password");
const match = await bcrypt.compare(password, hashedPassword);
if (!match) return res.status(401).send("Invalid password");
next();
};
Applied as:
app.get('/admin/dashboard', protectAdmin, (req, res) => {
res.send(cacheData);
});
This ensures the route remains inaccessible without proper authentication.
Integrating the React Frontend
The frontend is created using a standard React scaffold and built into static assets:
npm run build
The production build is served directly from Express:
app.use(express.static(path.join(__dirname, "frontend/build")));
Serving the frontend from the same origin as the API eliminates CORS complexity and simplifies deployment.
Deployment on Render
The application is deployed as a single Web Service on Render. The backend serves both API routes and static frontend assets.
Key deployment requirements:
- Install dependencies
- Build the React frontend during deployment
- Use the dynamic port provided by Render
const port = process.env.PORT || 3000;
app.listen(port);
Express Wildcard Route Issue on Render
During deployment, a routing error occurred related to wildcard route definitions such as:
app.get('*', ...)
and
app.get('/:path(*)', ...)
In the Render environment, newer versions of Express rely on a stricter version of path-to-regexp, which rejects certain wildcard patterns. This caused the application to crash with a PathError.
The resolution was to downgrade Express to a stable version below v5, specifically within the Express v4 range. Express v4 uses a more permissive routing parser and correctly handles wildcard routes such as:
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, "frontend/build", "index.html"));
});
Pinning Express to a stable v4 version in package.json resolved the deployment issue:
"express": "^4.18.2"
After redeployment, routing behaved as expected and the application started successfully.
Key Takeaways
- Version differences between local and production environments can affect routing behavior.
- Express v5 introduces stricter route parsing via
path-to-regexp. - Pinning dependencies explicitly prevents unexpected runtime incompatibilities.
- Serving React from the same Express backend simplifies CORS and deployment.
- Middleware provides clean protection for sensitive GET endpoints.
Conclusion
A traffic-splitting URL shortener may appear simple at first glance, but it involves considerations around routing, state management, authentication, frontend integration, and deployment environment consistency.
The most important lesson is not the redirect logic itself, but the operational awareness required to ship reliably. Dependency versions, route definitions, and hosting environments all influence behavior in production. Controlling these variables is essential for building stable full stack systems.
Top comments (0)