MT5 Trading Robot Showcase Site
Build a full-stack React and Express site with live Myfxbook trading data.
Introduction
30 Second Summary
Imagine you have built something brilliant, but no one can see the proof that it works. For trading robot developers, a professional showcase website with live, verified performance data is what turns skeptics into buyers.
In this project, you will build a full-stack showcase website for your MT5 trailing robots. The site displays live trading stats from Myfxbook, captures visitor emails for marketing, and links buyers directly to your MQL5 Market product pages.
What You'll Build
A React frontend paired with an Express.js backend that proxies Myfxbook API data, displays live trading metrics, and integrates Brevo for email capture.
By the end of this project, you'll have:
- A React frontend with product showcase cards, a live performance dashboard, and an email capture form.
- An Express.js backend that securely proxies the Myfxbook API using the Backend-for-Frontend (BFF) pattern, keeping your credentials out of the browser.
- Brevo email integration that captures visitor emails and adds them to your marketing contact list.
- Secret Mission: Add a custom robot services request form where visitors can submit details about a trading robot they want built.
Are there any prerequisites?
You should have basic JavaScript knowledge and familiarity with HTML/CSS. You also need a Myfxbook account with at least one connected trading account to pull live performance data.
Before We Start
Before We Start...
Set Up the Project Structure
You're building a full-stack web application that showcases your MT5 trading robots with live performance data from Myfxbook. Every full-stack app needs two pieces: a frontend that visitors see in their browser, and a backend that handles sensitive operations like API authentication.
In this step, you'll create the project structure with a React frontend (powered by Vite) and an Express.js backend. By the end, both servers will be running on your machine.
In this step, get ready to:
- Scaffold the React frontend with Vite.
- Initialize the Express backend with its dependencies.
- Create the server configuration files and verify both servers run.
Scaffold the frontend and backend projects
Your project needs two separate directories: client for the React frontend and server for the Node.js backend. You'll scaffold both from your terminal.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir mt5-showcase
cd mt5-showcase
- Scaffold the React frontend by running this command:
npm create vite@latest client -- --template react
What does this command do?
npm create vite@latest downloads and runs the Vite project scaffolding tool. The client argument names the folder to create, and --template react tells Vite to set up a React project.
If npm asks you to confirm installing a package, type y and press Enter to proceed.
You should see a message saying the project was scaffolded successfully, along with instructions to cd into the folder and install dependencies.
- Install the React dependencies by running these commands:
cd client
npm install
cd ..
Why cd back to the project root?
cd .. moves you back up to the mt5-showcase folder. You need to be in the project root to create the server directory next.
Seeing an npm error?
Make sure you have an active internet connection. The npm install command downloads packages from the npm registry.
help me debug an npm install error in my Vite React project
- Create the server directory and initialize a Node.js package by running these commands:
mkdir server
cd server
npm init -y
You should see output confirming a package.json file was created in the server folder.
- Install the Express backend dependencies by running this command:
npm install express cors dotenv
What are these packages?
- express is a web framework for building your backend API routes.
- cors enables your React frontend to make requests to the Express backend during development.
- dotenv loads your secret credentials from a .env file into your code, so you never hard-code passwords.
You should see output showing three packages were added successfully.
Install failed?
Double check you are inside the server directory before running the install command. You can confirm with pwd (macOS) or cd (Windows) to print your current directory.
help me fix an npm install error for express, cors, and dotenv
- Verify the project folder structure by running this command:
cd ..
ls
You should see both client and server listed in the output.
Create the environment and server files
Your backend needs two files: a .env file to store your API credentials securely, and an index.js file to run the Express server. You'll create both in VS Code.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the mt5-showcase folder, then click Open.
You should see the client and server folders in the VS Code Explorer sidebar on the left.
- In the Explorer sidebar, right-click on the server folder.
- Click New File.
- Type .env and press Enter.
- Paste the following placeholder values into .env:
MYFXBOOK_EMAIL=your-myfxbook-email@example.com
MYFXBOOK_PASSWORD=your-myfxbook-password
BREVO_API_KEY=your-brevo-api-key
BREVO_LIST_ID=2
PORT=3001
What are these values?
- MYFXBOOK_EMAIL and MYFXBOOK_PASSWORD are your Myfxbook account credentials. Replace these with your real values now.
- BREVO_API_KEY and BREVO_LIST_ID are placeholders you'll fill in during a later step when you set up Brevo.
- PORT sets which port the Express server listens on.
- Replace your-myfxbook-email@example.com with the email address on your Myfxbook account.
- Replace your-myfxbook-password with your Myfxbook account password.
- Leave BREVO_API_KEY and BREVO_LIST_ID as-is for now.
- Save .env by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Keep your .env file private
The .env file contains your real account credentials. Never share it publicly or commit it to Git.
✔️ Awesome, I've got everything!
Great. Double check you've saved your .env file with your real Myfxbook credentials filled in.
ⓧ I'd like to double check the full code
Your server/.env file should look like this (with your actual Myfxbook email and password):
MYFXBOOK_EMAIL=your-myfxbook-email@example.com
MYFXBOOK_PASSWORD=your-myfxbook-password
BREVO_API_KEY=your-brevo-api-key
BREVO_LIST_ID=2
PORT=3001
Now you'll create the Express server file.
- In the Explorer sidebar, right-click on the server folder.
- Click New File.
- Type index.js and press Enter.
- Paste the following code into index.js:
// Load required packages
const express = require('express');
const cors = require('cors');
require('dotenv').config();
// Create the Express app and set the port from .env
const app = express();
const PORT = process.env.PORT || 3001;
// Enable CORS so the React frontend can call these API routes
app.use(cors());
// Parse incoming JSON request bodies
app.use(express.json());
// Health check endpoint to verify the server is running
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
What does this code do?
- require('dotenv').config() loads your .env values (like PORT and your Myfxbook credentials) into process.env.
- app.use(cors()) allows your React dev server on port 5173 to make requests to this backend on port 3001.
- The /api/health route is a simple endpoint that returns {"status": "ok"}. You'll use it to confirm the server is running.
- Save index.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).
VS Code not showing your server folder?
Make sure you opened the mt5-showcase folder (the project root), not the client or server folder directly. Both subfolders should be visible in the sidebar.
help me open the correct project folder in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved your index.js file.
ⓧ I'd like to double check the full code
Your server/index.js should look like this:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Start and verify both servers
You now have everything in place to run both the backend and the frontend. You'll start each server in its own terminal inside VS Code.
- Open the VS Code integrated terminal by pressing Ctrl+` (macOS and Windows).
- Start the Express server by running these commands:
cd server
node index.js
You should see Server running on http://localhost:3001 printed in the terminal.
Seeing an error instead?
If you see Cannot find module, make sure you ran npm install express cors dotenv earlier inside the server folder. Run it again now if needed, then try node index.js again.
help me fix a Cannot find module error when starting my Express server
- Open a second terminal by clicking the + icon in the terminal panel's top-right corner.
- Start the React dev server by running these commands:
cd client
npm run dev
You should see output with a local URL, typically http://localhost:5173.
- Open your browser and navigate to http://localhost:5173.
You should see the default Vite React page with the Vite and React logos and a counter button.
- Open a new browser tab and navigate to http://localhost:3001/api/health.
You should see a JSON response: {"status":"ok"}. This confirms your Express backend is running and responding to requests.
Browser shows an error for localhost:3001?
Switch back to the first terminal in VS Code and check that it still shows Server running on http://localhost:3001. If it crashed, look for an error message and fix the issue in server/index.js before restarting with node index.js.
help me debug why my Express server is not responding on localhost:3001
Why two separate servers?
During development, Vite runs a fast dev server for your React frontend on port 5173. Express runs your backend API on port 3001.
This separation keeps your frontend code and backend logic independent. The frontend handles what visitors see, while the backend securely manages API credentials and proxies data from Myfxbook.
Your project structure is ready with both frontend and backend servers running. Next up, you'll build the Myfxbook API proxy that securely fetches your live trading data.
Build the Myfxbook API Proxy
In the previous step, you set up a React frontend and an Express backend. Your server is running with a health check route, and your .env file has placeholders for your Myfxbook credentials.
Now you need to connect your server to the Myfxbook API so your website can display live trading performance. The Myfxbook API does not support browser-based requests due to CORS restrictions. Your Express server will act as a proxy, fetching trading data on behalf of the frontend.
In this step, get ready to:
- Create the Myfxbook authentication module with session caching.
- Build API routes that proxy account and performance data.
- Test the proxy to confirm live trading data flows through your server.
Create the Myfxbook authentication module
The Myfxbook API uses session-based authentication. You log in with your email and password, receive a session ID, and then pass that session ID to every subsequent API call.
Why a backend proxy instead of calling Myfxbook from the browser?
The Myfxbook API blocks cross-origin requests from browsers. If your React app tried to call Myfxbook directly, the browser would reject the response.
This is a common pattern called Backend-for-Frontend (BFF). Your Express server sits between the browser and Myfxbook, making API calls server-side where CORS doesn't apply. It also keeps your Myfxbook credentials out of the browser.
- In the Explorer sidebar in VS Code, right-click the server folder.
- Select New File and name it myfxbook.js.
- Add the session management and login logic by pasting the following code into myfxbook.js:
let sessionId = null;
async function login() {
const email = encodeURIComponent(process.env.MYFXBOOK_EMAIL);
const password = encodeURIComponent(process.env.MYFXBOOK_PASSWORD);
const url = `https://www.myfxbook.com/api/login.json?email=${email}&password=${password}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
throw new Error(`Myfxbook login failed: ${data.message}`);
}
sessionId = data.session;
return sessionId;
}
async function getSession() {
if (!sessionId) {
await login();
}
return sessionId;
}
What does this code do?
- sessionId is a module-level variable that caches the current Myfxbook session. This avoids logging in on every single API request.
- login() calls the Myfxbook login.json endpoint with your email and password. On success, it stores the returned session ID.
- getSession() is a lazy helper. It only calls login() if no session exists yet. Every other function in this module uses getSession() to get a valid session.
- Next, add the getMyAccounts() function below getSession() in myfxbook.js:
async function getMyAccounts() {
const session = await getSession();
const url = `https://www.myfxbook.com/api/get-my-accounts.json?session=${session}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
sessionId = null;
await login();
const retryUrl = `https://www.myfxbook.com/api/get-my-accounts.json?session=${sessionId}`;
const retryResponse = await fetch(retryUrl);
return await retryResponse.json();
}
return data;
}
What does this code do?
getMyAccounts() fetches your list of trading accounts from Myfxbook. If the session has expired (the API returns an error), it clears the cached session, logs in again, and retries the request automatically.
Myfxbook sessions are IP-bound and expire after 1 month. The retry logic handles cases where a stored session becomes invalid between requests.
- Finally, add the getDataDaily() function and the module exports below getMyAccounts():
async function getDataDaily(accountId, startDate, endDate) {
const session = await getSession();
const url = `https://www.myfxbook.com/api/get-data-daily.json?session=${session}&id=${accountId}&start=${startDate}&end=${endDate}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
sessionId = null;
await login();
const retryUrl = `https://www.myfxbook.com/api/get-data-daily.json?session=${sessionId}&id=${accountId}&start=${startDate}&end=${endDate}`;
const retryResponse = await fetch(retryUrl);
return await retryResponse.json();
}
return data;
}
module.exports = { getMyAccounts, getDataDaily };
What does this code do?
getDataDaily() fetches daily performance data for a specific account. It takes an account ID, start date, and end date, and returns data points for balance, pips, lots, and profit. It uses the same session retry pattern as getMyAccounts().
The module.exports line exposes both functions so your route files can import and use them.
- Save myfxbook.js.
You won't see a change yet. The next substep creates the routes that use this module.
✔️ Awesome, I've got everything!
Great. Double check you've saved server/myfxbook.js before moving on.
ⓧ I'd like to double check the full code
let sessionId = null;
async function login() {
const email = encodeURIComponent(process.env.MYFXBOOK_EMAIL);
const password = encodeURIComponent(process.env.MYFXBOOK_PASSWORD);
const url = `https://www.myfxbook.com/api/login.json?email=${email}&password=${password}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
throw new Error(`Myfxbook login failed: ${data.message}`);
}
sessionId = data.session;
return sessionId;
}
async function getSession() {
if (!sessionId) {
await login();
}
return sessionId;
}
async function getMyAccounts() {
const session = await getSession();
const url = `https://www.myfxbook.com/api/get-my-accounts.json?session=${session}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
sessionId = null;
await login();
const retryUrl = `https://www.myfxbook.com/api/get-my-accounts.json?session=${sessionId}`;
const retryResponse = await fetch(retryUrl);
return await retryResponse.json();
}
return data;
}
async function getDataDaily(accountId, startDate, endDate) {
const session = await getSession();
const url = `https://www.myfxbook.com/api/get-data-daily.json?session=${session}&id=${accountId}&start=${startDate}&end=${endDate}`;
const response = await fetch(url);
const data = await response.json();
if (data.error) {
sessionId = null;
await login();
const retryUrl = `https://www.myfxbook.com/api/get-data-daily.json?session=${sessionId}&id=${accountId}&start=${startDate}&end=${endDate}`;
const retryResponse = await fetch(retryUrl);
return await retryResponse.json();
}
return data;
}
module.exports = { getMyAccounts, getDataDaily };
Not sure about the file location?
myfxbook.js should be directly inside the server folder, at the same level as index.js and package.json. It should not be inside any subfolder.
Help me figure out where to put myfxbook.js in my project
Create the API routes for account and performance data
Now you need Express routes that the React frontend can call. These routes will use the Myfxbook module you just created to fetch data and return it as JSON.
- In the Explorer sidebar, right-click the server folder.
- Select New Folder and name it routes.
- Right-click the new routes folder and select New File.
- Name the file myfxbook.js.
- Add the imports and the accounts route by pasting the following code into routes/myfxbook.js:
const express = require('express');
const router = express.Router();
const { getMyAccounts, getDataDaily } = require('../myfxbook');
router.get('/accounts', async (req, res) => {
try {
const data = await getMyAccounts();
res.json(data);
} catch (error) {
console.error('Error fetching accounts:', error.message);
res.status(500).json({ error: 'Failed to fetch account data' });
}
});
What does this code do?
- express.Router() creates a modular set of routes that can be mounted onto the main Express app.
- The require('../myfxbook') import pulls in the two helper functions from the authentication module you just built.
- The GET /accounts route calls getMyAccounts() and returns the result as JSON. If anything goes wrong, it returns a 500 error with a message.
- Add the performance route and the module export below the accounts route in the same file:
router.get('/performance/:accountId', async (req, res) => {
try {
const { accountId } = req.params;
const startDate = req.query.start || '2024-01-01';
const endDate = req.query.end || new Date().toISOString().split('T')[0];
const data = await getDataDaily(accountId, startDate, endDate);
res.json(data);
} catch (error) {
console.error('Error fetching performance:', error.message);
res.status(500).json({ error: 'Failed to fetch performance data' });
}
});
module.exports = router;
What does this code do?
The GET /performance/:accountId route accepts an account ID as a URL parameter. It reads optional start and end query parameters for the date range. If no dates are provided, it defaults to January 1, 2024 through today.
module.exports = router makes the router available so index.js can mount it.
- Save routes/myfxbook.js.
✔️ Awesome, I've got everything!
Great. Double check you've saved server/routes/myfxbook.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const router = express.Router();
const { getMyAccounts, getDataDaily } = require('../myfxbook');
router.get('/accounts', async (req, res) => {
try {
const data = await getMyAccounts();
res.json(data);
} catch (error) {
console.error('Error fetching accounts:', error.message);
res.status(500).json({ error: 'Failed to fetch account data' });
}
});
router.get('/performance/:accountId', async (req, res) => {
try {
const { accountId } = req.params;
const startDate = req.query.start || '2024-01-01';
const endDate = req.query.end || new Date().toISOString().split('T')[0];
const data = await getDataDaily(accountId, startDate, endDate);
res.json(data);
} catch (error) {
console.error('Error fetching performance:', error.message);
res.status(500).json({ error: 'Failed to fetch performance data' });
}
});
module.exports = router;
Now you need to wire these routes into the main Express server.
- Switch back to server/index.js in VS Code.
- Find this line near the top of the file:
require('dotenv').config();
- Add the following import directly below it:
const myfxbookRoutes = require('./routes/myfxbook');
- Next, find this block:
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
- Add the following line directly below the health check route:
app.use('/api', myfxbookRoutes);
What does this line do?
app.use('/api', myfxbookRoutes) mounts the router at the /api path. Since the router defines /accounts and /performance/:accountId, the full paths become /api/accounts and /api/performance/:accountId.
- Save index.js.
✔️ Awesome, I've got everything!
Great. Double check you've saved server/index.js before moving on.
ⓧ I'd like to double check the full code
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const myfxbookRoutes = require('./routes/myfxbook');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.use('/api', myfxbookRoutes);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Seeing a 'Cannot find module' error?
Make sure the routes folder is directly inside server, and myfxbook.js is inside routes. The import path './routes/myfxbook' in index.js must match this exact folder structure.
Help me debug a Cannot find module error in my Express server
Test the proxy endpoints
The code is in place. Before testing, you need to add your real Myfxbook credentials to the .env file so the proxy can actually log in.
- Switch back to server/.env in VS Code.
- Replace your-myfxbook-email@example.com with the email address you use to sign in to Myfxbook.
- Replace your-myfxbook-password with your Myfxbook password.
- Save .env.
Keep your .env file private
Never commit .env to version control. It contains your real password. If you plan to push this project to GitHub later, add .env to your .gitignore file.
- In the terminal where your Express server is running, press Ctrl+C to stop the server.
- Restart the server by running:
node index.js
- Open your browser and navigate to:
http://localhost:3001/api/accounts
You should see JSON data from Myfxbook containing your trading accounts with fields like id, gain, daily, monthly, and drawdown.
Seeing an error or empty response?
- If you see Myfxbook login failed, double check your email and password in .env. Make sure there are no extra spaces around the values.
- If you see Cannot find module, verify that server/myfxbook.js and server/routes/myfxbook.js both exist in the correct locations.
- Make sure you stopped and restarted the server after saving your changes. Express does not hot-reload.
Help me debug my Myfxbook API proxy
Your backend proxy is live and pulling real trading data from Myfxbook. Next up, you'll build the React frontend to display your robots and their performance stats.
Build the React Product Showcase
Your backend is live and ready to serve Myfxbook trading data through the proxy. Now it's time to build the frontend that your visitors actually see.
In this step, you'll create the React product showcase. You'll build a professional dark-themed layout with a navigation bar, a hero banner, and product cards that link directly to your MQL5 Market pages.
In this step, get ready to:
- Create the app layout with a navbar and hero section.
- Build a reusable ProductCard component for displaying each robot.
- Add product data for your MT5 trailing robots.
Create the app layout and styles
The Vite scaffold created default content in App.jsx and App.css. You'll replace both files with your own layout and dark theme.
- In VS Code, open client/src/App.jsx from the Explorer sidebar.
- Select all the existing content and replace it with this code:
import ProductCard from './components/ProductCard';
import products from './data/products';
import './App.css';
function App() {
return (
<div className="app">
{/* Navigation bar with brand name */}
<nav className="navbar">
<h1 className="brand">MT5 Trading Robots</h1>
</nav>
{/* Hero banner with tagline */}
<section className="hero">
<h2>Automated Trading Solutions</h2>
<p>Professional MT5 trailing robots backed by verified live performance.</p>
</section>
{/* Product cards grid */}
<section className="section" id="products">
<h2>Our Robots</h2>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
</div>
);
}
export default App;
What does this code do?
This file defines the main structure of your showcase page using JSX, which looks like HTML but lives inside JavaScript.
- The navbar section renders your brand name at the top of the page.
- The hero section displays a headline and tagline to hook visitors.
- The products section loops through the products array and renders a ProductCard for each entry.
- Save App.jsx.
Your browser will show an error because ProductCard and products don't exist yet. You'll create both in the next substeps.
Next, set up the dark theme. The stylesheet controls the professional look of the entire page.
- Open client/src/App.css in the Explorer sidebar.
- Select all the existing content and replace it with this code:
/* Reset default browser margins and padding */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Dark background for the whole page */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0a0e17;
color: #e0e0e0;
}
.app {
min-height: 100vh;
}
/* Top navigation bar */
.navbar {
display: flex;
align-items: center;
padding: 1rem 2rem;
background-color: #111827;
border-bottom: 1px solid #1f2937;
}
/* Brand name in green accent */
.brand {
font-size: 1.5rem;
color: #10b981;
}
/* Hero banner with gradient background */
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
}
.hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #ffffff;
}
.hero p {
font-size: 1.2rem;
color: #9ca3af;
}
/* Reusable section container */
.section {
padding: 3rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
color: #ffffff;
}
/* Responsive grid for product cards */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
/* Individual product card */
.product-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.product-card h3 {
color: #10b981;
margin-bottom: 0.75rem;
}
.product-card p {
color: #9ca3af;
margin-bottom: 1rem;
line-height: 1.6;
}
/* Feature list inside each card */
.features-list {
list-style: none;
margin-bottom: 1.5rem;
}
.features-list li {
padding: 0.25rem 0;
color: #d1d5db;
}
/* Green plus icon before each feature */
.features-list li::before {
content: '+ ';
color: #10b981;
}
/* CTA button linking to MQL5 Market */
.buy-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s;
}
.buy-button:hover {
background-color: #059669;
}
What does this stylesheet do?
- The global reset (* selector) removes default browser margins so the layout is consistent across browsers.
- The background color #0a0e17 creates a deep dark base, while #10b981 provides a green accent for branding and buttons.
- The .products-grid uses CSS Grid with auto-fit so cards automatically wrap onto new rows on smaller screens.
- Save App.css.
You won't see a change yet because the app still can't find the imported files. The next substep creates the ProductCard component.
Seeing a blank white page instead of errors?
Make sure you saved App.jsx after replacing the content. If the Vite dev server stopped, switch back to the terminal running it and restart with npm run dev.
help me debug my Vite dev server not showing any errors or content
Create the ProductCard component
Each trading robot needs its own card showing the name, description, feature list, and a purchase link. You'll build this as a reusable component that receives product data through props.
- In the Explorer sidebar, right-click the client/src folder.
- Click New Folder and name it components.
- Right-click the new components folder, click New File, and name it ProductCard.jsx.
- Add the following code to ProductCard.jsx:
function ProductCard({ product }) {
return (
<div className="product-card">
{/* Robot name */}
<h3>{product.name}</h3>
{/* Short description */}
<p>{product.description}</p>
{/* List of key features */}
<ul className="features-list">
{product.features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
{/* Purchase link opens the MQL5 Market in a new tab */}
<a
href={product.mql5Url}
target="_blank"
rel="noopener noreferrer"
className="buy-button"
>
Buy on MQL5 Market
</a>
</div>
);
}
export default ProductCard;
What does this component do?
- The function receives a product object as a prop, then renders the robot's name, description, and features from that object.
- The features.map() call loops through the features array and creates a list item for each one.
- The target="_blank" attribute on the anchor tag opens the MQL5 Market link in a new browser tab so visitors don't leave your site.
- Save ProductCard.jsx.
You won't see a change yet because the products data file is still missing. The next substep creates it.
Add your product data
The product cards need data to display. You'll create a separate data file that holds your robot entries, keeping the data cleanly separated from the component logic.
- In the Explorer sidebar, right-click the client/src folder.
- Click New Folder and name it data.
- Right-click the new data folder, click New File, and name it products.js.
- Add the following code to products.js:
const products = [
{
id: 1,
name: 'Trailing Pro EA',
description: 'An advanced trailing stop expert advisor for MT5 that dynamically adjusts stop-loss levels based on market volatility.',
features: [
'Dynamic trailing stop based on ATR',
'Multi-timeframe analysis',
'Risk management built-in',
'Works on all currency pairs',
],
// Replace with your actual MQL5 Market product URL
mql5Url: 'https://www.mql5.com/en/market',
// Link to a Myfxbook account for live performance (added in a later step)
myfxbookAccountId: '',
},
{
id: 2,
name: 'Gold Trailer EA',
description: 'Specialized trailing robot optimized for XAUUSD gold trading with precision entry and exit management.',
features: [
'Optimized for XAUUSD',
'Smart lot sizing',
'Breakeven protection',
'Low drawdown design',
],
mql5Url: 'https://www.mql5.com/en/market',
myfxbookAccountId: '',
},
];
export default products;
What does this data file do?
- Each product object has an id, name, description, and a features array that the ProductCard component reads to render each card.
- The mql5Url field holds the link for the "Buy on MQL5 Market" button. Replace the placeholder URLs with your actual MQL5 product page links.
- The myfxbookAccountId is empty for now. You'll use it in the next step to connect each robot to its live performance data.
- Save products.js.
- Switch to your browser and check http://localhost:5173. You should see a dark page with a green "MT5 Trading Robots" brand in the navbar, a hero section with the headline "Automated Trading Solutions", and two product cards in a grid below.
Don't see the product cards?
- Check that all three files are saved: App.jsx, App.css, ProductCard.jsx, and products.js.
- Make sure the folder structure matches exactly: client/src/components/ProductCard.jsx and client/src/data/products.js. A misplaced file causes an import error.
- If the browser shows a module error, check the terminal running Vite for details on which import path failed.
Help me fix my React product cards not rendering
Customize with your own robots
The sample data uses placeholder robot names. You can update the name, description, features, and mql5Url fields in products.js to match your actual MT5 robots and MQL5 Market product pages.
✔️ Awesome, I've got everything!
Great. Double check you've saved all four files before moving on.
ⓧ I'd like to double check the full code
Here is the complete code for each file. Compare yours against these:
import ProductCard from './components/ProductCard';
import products from './data/products';
import './App.css';
function App() {
return (
<div className="app">
<nav className="navbar">
<h1 className="brand">MT5 Trading Robots</h1>
</nav>
<section className="hero">
<h2>Automated Trading Solutions</h2>
<p>Professional MT5 trailing robots backed by verified live performance.</p>
</section>
<section className="section" id="products">
<h2>Our Robots</h2>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
</div>
);
}
export default App;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0a0e17;
color: #e0e0e0;
}
.app {
min-height: 100vh;
}
.navbar {
display: flex;
align-items: center;
padding: 1rem 2rem;
background-color: #111827;
border-bottom: 1px solid #1f2937;
}
.brand {
font-size: 1.5rem;
color: #10b981;
}
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
}
.hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #ffffff;
}
.hero p {
font-size: 1.2rem;
color: #9ca3af;
}
.section {
padding: 3rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
color: #ffffff;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.product-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.product-card h3 {
color: #10b981;
margin-bottom: 0.75rem;
}
.product-card p {
color: #9ca3af;
margin-bottom: 1rem;
line-height: 1.6;
}
.features-list {
list-style: none;
margin-bottom: 1.5rem;
}
.features-list li {
padding: 0.25rem 0;
color: #d1d5db;
}
.features-list li::before {
content: '+ ';
color: #10b981;
}
.buy-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s;
}
.buy-button:hover {
background-color: #059669;
}
function ProductCard({ product }) {
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>{product.description}</p>
<ul className="features-list">
{product.features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
<a
href={product.mql5Url}
target="_blank"
rel="noopener noreferrer"
className="buy-button"
>
Buy on MQL5 Market
</a>
</div>
);
}
export default ProductCard;
const products = [
{
id: 1,
name: 'Trailing Pro EA',
description: 'An advanced trailing stop expert advisor for MT5 that dynamically adjusts stop-loss levels based on market volatility.',
features: [
'Dynamic trailing stop based on ATR',
'Multi-timeframe analysis',
'Risk management built-in',
'Works on all currency pairs',
],
mql5Url: 'https://www.mql5.com/en/market',
myfxbookAccountId: '',
},
{
id: 2,
name: 'Gold Trailer EA',
description: 'Specialized trailing robot optimized for XAUUSD gold trading with precision entry and exit management.',
features: [
'Optimized for XAUUSD',
'Smart lot sizing',
'Breakeven protection',
'Low drawdown design',
],
mql5Url: 'https://www.mql5.com/en/market',
myfxbookAccountId: '',
},
];
export default products;
Your product showcase is looking sharp. Next up, you'll pull live trading performance data from Myfxbook and display it right alongside these product cards.
Display Live Myfxbook Performance Data
Your product showcase looks great, but visitors have no reason to trust your robots yet. Static descriptions and feature lists only go so far.
In this step, you'll pull live trading performance data from your Myfxbook accounts through your Express backend proxy and display it directly on your site. Real numbers like gain percentages and drawdown stats speak louder than marketing copy.
In this step, get ready to:
- Create React components that display live trading performance metrics.
- Fetch account data from your Express proxy using React hooks.
- Configure the Vite dev server proxy to connect the frontend to your backend.
Create the PerformanceCard component
Each trading account needs its own card that displays key metrics like gain, drawdown, and daily and monthly performance. This component receives account data as a prop and renders it in a structured layout.
- Create a new file called PerformanceCard.jsx inside client/src/components/ and paste the following code:
function PerformanceCard({ account }) {
return (
<div className="performance-card">
<h3>{account.name}</h3>
<div className="metrics">
<div className="metric">
<span className="metric-label">Total Gain</span>
<span className="metric-value gain">{account.gain}%</span>
</div>
<div className="metric">
<span className="metric-label">Drawdown</span>
<span className="metric-value drawdown">{account.drawdown}%</span>
</div>
<div className="metric">
<span className="metric-label">Daily</span>
<span className="metric-value">{account.daily}%</span>
</div>
<div className="metric">
<span className="metric-label">Monthly</span>
<span className="metric-value">{account.monthly}%</span>
</div>
</div>
</div>
);
}
export default PerformanceCard;
What does this code do?
This component takes an account object as a prop and renders four key trading metrics in a two-column grid layout.
- The gain and drawdown CSS classes color-code values green for gains and red for drawdown, giving visitors instant visual feedback.
- The component is purely presentational. It receives data and displays it. A separate dashboard component will handle the data fetching.
- Save PerformanceCard.jsx.
You won't see a change yet. The next component will use this card to build a full dashboard.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
function PerformanceCard({ account }) {
return (
<div className="performance-card">
<h3>{account.name}</h3>
<div className="metrics">
<div className="metric">
<span className="metric-label">Total Gain</span>
<span className="metric-value gain">{account.gain}%</span>
</div>
<div className="metric">
<span className="metric-label">Drawdown</span>
<span className="metric-value drawdown">{account.drawdown}%</span>
</div>
<div className="metric">
<span className="metric-label">Daily</span>
<span className="metric-value">{account.daily}%</span>
</div>
<div className="metric">
<span className="metric-label">Monthly</span>
<span className="metric-value">{account.monthly}%</span>
</div>
</div>
</div>
);
}
export default PerformanceCard;
Build the PerformanceDashboard and connect it to the app
The dashboard component is where data fetching happens. It calls your Express proxy's /api/accounts endpoint when the component mounts, manages loading and error states, and renders a PerformanceCard for each account returned.
- Create a new file called PerformanceDashboard.jsx inside client/src/components/ and paste the following code:
import { useState, useEffect } from 'react';
import PerformanceCard from './PerformanceCard';
function PerformanceDashboard() {
const [accounts, setAccounts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchAccounts() {
try {
const response = await fetch('/api/accounts');
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
setAccounts(data.accounts || []);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchAccounts();
}, []);
if (loading) return <p className="loading">Loading performance data...</p>;
if (error) return <p className="error">Unable to load performance data.</p>;
return (
<div className="performance-dashboard">
{accounts.map((account) => (
<PerformanceCard key={account.id} account={account} />
))}
</div>
);
}
export default PerformanceDashboard;
What does this code do?
- useEffect runs fetchAccounts once when the component first mounts. The empty dependency array [] ensures it only fires on the initial render.
- Three state variables track the accounts data, loading status, and any errors. The finally block sets loading to false whether the fetch succeeds or fails.
- If the API returns an error, the dashboard shows a friendly error message instead of crashing. If data loads successfully, it maps over the accounts and renders a PerformanceCard for each one.
- Save PerformanceDashboard.jsx.
You won't see a change yet. Next, you'll add the performance styles and wire the dashboard into your main app layout.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before moving on.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react';
import PerformanceCard from './PerformanceCard';
function PerformanceDashboard() {
const [accounts, setAccounts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchAccounts() {
try {
const response = await fetch('/api/accounts');
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
setAccounts(data.accounts || []);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchAccounts();
}, []);
if (loading) return <p className="loading">Loading performance data...</p>;
if (error) return <p className="error">Unable to load performance data.</p>;
return (
<div className="performance-dashboard">
{accounts.map((account) => (
<PerformanceCard key={account.id} account={account} />
))}
</div>
);
}
export default PerformanceDashboard;
Now you need styles for the performance cards and dashboard grid. These follow the same dark theme you set up earlier.
- In client/src/App.css, scroll to the bottom of the file (after the .buy-button:hover rule) and paste these styles:
.performance-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.performance-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.performance-card h3 {
color: #ffffff;
margin-bottom: 1rem;
}
.metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.metric {
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.85rem;
color: #9ca3af;
}
.metric-value {
font-size: 1.4rem;
font-weight: 700;
color: #e0e0e0;
}
.metric-value.gain {
color: #10b981;
}
.metric-value.drawdown {
color: #ef4444;
}
.loading,
.error {
text-align: center;
padding: 2rem;
color: #9ca3af;
}
What do these styles do?
- The .performance-dashboard uses a responsive grid, just like your product cards.
- The .metric-value.gain turns gains green and .metric-value.drawdown turns drawdown red, giving visitors instant visual feedback on performance.
- Save App.css.
You'll see these styles applied in a moment when you update App.jsx.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before moving on.
ⓧ I'd like to double check the full code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0a0e17;
color: #e0e0e0;
}
.app {
min-height: 100vh;
}
.navbar {
display: flex;
align-items: center;
padding: 1rem 2rem;
background-color: #111827;
border-bottom: 1px solid #1f2937;
}
.brand {
font-size: 1.5rem;
color: #10b981;
}
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
}
.hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #ffffff;
}
.hero p {
font-size: 1.2rem;
color: #9ca3af;
}
.section {
padding: 3rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
color: #ffffff;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.product-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.product-card h3 {
color: #10b981;
margin-bottom: 0.75rem;
}
.product-card p {
color: #9ca3af;
margin-bottom: 1rem;
line-height: 1.6;
}
.features-list {
list-style: none;
margin-bottom: 1.5rem;
}
.features-list li {
padding: 0.25rem 0;
color: #d1d5db;
}
.features-list li::before {
content: '+ ';
color: #10b981;
}
.buy-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s;
}
.buy-button:hover {
background-color: #059669;
}
.performance-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.performance-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.performance-card h3 {
color: #ffffff;
margin-bottom: 1rem;
}
.metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.metric {
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.85rem;
color: #9ca3af;
}
.metric-value {
font-size: 1.4rem;
font-weight: 700;
color: #e0e0e0;
}
.metric-value.gain {
color: #10b981;
}
.metric-value.drawdown {
color: #ef4444;
}
.loading,
.error {
text-align: center;
padding: 2rem;
color: #9ca3af;
}
The last piece is connecting the dashboard to your main app layout. You'll import the PerformanceDashboard component and add a new performance section below the products.
- Replace the entire contents of client/src/App.jsx with the updated code below:
import ProductCard from './components/ProductCard';
import PerformanceDashboard from './components/PerformanceDashboard';
import products from './data/products';
import './App.css';
function App() {
return (
<div className="app">
<nav className="navbar">
<h1 className="brand">MT5 Trading Robots</h1>
</nav>
<section className="hero">
<h2>Automated Trading Solutions</h2>
<p>Professional MT5 trailing robots backed by verified live performance.</p>
</section>
<section className="section" id="products">
<h2>Our Robots</h2>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
<section className="section" id="performance">
<h2>Live Performance</h2>
<p>Real-time results from Myfxbook verified accounts.</p>
<PerformanceDashboard />
</section>
</div>
);
}
export default App;
What changed?
- A new import for PerformanceDashboard was added at the top of the file.
- A new <section> with the id performance was added after the products section. It renders a heading, a subtitle, and the PerformanceDashboard component.
- Save App.jsx and refresh your browser at http://localhost:5173.
- You should see a "Live Performance" heading below your product cards. The dashboard will show "Unable to load performance data" because the Vite proxy isn't configured yet.
Don't see the Live Performance section?
- Make sure App.jsx is saved. Check your browser console for any import errors pointing to PerformanceDashboard.
- Confirm that both PerformanceCard.jsx and PerformanceDashboard.jsx are inside the client/src/components/ folder. A misplaced file causes a module-not-found error.
- Why isn't my PerformanceDashboard rendering in the browser?
✔️ Awesome, I've got everything!
Great. Double check you've saved App.jsx before continuing.
ⓧ I'd like to double check the full code
import ProductCard from './components/ProductCard';
import PerformanceDashboard from './components/PerformanceDashboard';
import products from './data/products';
import './App.css';
function App() {
return (
<div className="app">
<nav className="navbar">
<h1 className="brand">MT5 Trading Robots</h1>
</nav>
<section className="hero">
<h2>Automated Trading Solutions</h2>
<p>Professional MT5 trailing robots backed by verified live performance.</p>
</section>
<section className="section" id="products">
<h2>Our Robots</h2>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
<section className="section" id="performance">
<h2>Live Performance</h2>
<p>Real-time results from Myfxbook verified accounts.</p>
<PerformanceDashboard />
</section>
</div>
);
}
export default App;
Configure the Vite dev server proxy
Right now the dashboard shows an error because the React dev server on port 5173 doesn't know where to send /api requests. Your Express server is running on port 3001, so you need a proxy to bridge the two during development.
What is a dev server proxy?
Vite can intercept requests that match a pattern (like /api) and forward them to a different server. The browser thinks it's talking to the Vite dev server, but behind the scenes Vite passes those requests to your Express backend.
This eliminates CORS issues during development because the browser sees all requests going to the same origin.
- Open client/vite.config.js and replace the entire contents with this updated configuration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
What does this configuration do?
- The server.proxy block tells Vite to forward any request starting with /api to your Express server at http://localhost:3001.
- changeOrigin: true modifies the request's origin header to match the target, which prevents the backend from rejecting the request.
- Save vite.config.js.
- Stop the React dev server by pressing Ctrl+C in the terminal where it's running.
- Restart it by running:
npm run dev
Why restart?
Changes to vite.config.js require a full restart because the config is loaded once when the dev server starts. Hot module replacement doesn't pick up config changes.
- Make sure your Express server is still running in its own terminal on port 3001.
- Open http://localhost:5173 in your browser. You should see a "Live Performance" section below your product cards with performance cards showing your Myfxbook account data. Each card displays the account name, total gain %, drawdown %, daily %, and monthly %.
Still seeing "Unable to load performance data"?
- Check that you restarted the Vite dev server after saving vite.config.js. A page refresh alone is not enough.
- Confirm your Express server is running on port 3001. Open http://localhost:3001/api/health directly in your browser to verify.
- Verify that your server/.env file contains your real Myfxbook email and password, not the placeholder values.
- Why is my Myfxbook performance data not loading in the browser?
✔️ Awesome, I've got everything!
Great. Your Vite config is all set.
ⓧ I'd like to double check the full code
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
Your site now shows live trading performance data pulled straight from Myfxbook. Visitors can see real gain percentages and drawdown figures that prove your robots perform. Next up, you'll add an email capture system so visitors can subscribe to your updates.
Add Email Capture with Brevo
Your showcase site now has product cards and live performance data. The missing piece is a way to capture visitor interest.
In this step, you will connect Brevo (an email marketing platform) to your Express backend and build a signup form in React. When a visitor enters their email, your backend calls the Brevo API to add them to your marketing list.
In this step, get ready to:
- Set up a free Brevo account and generate an API key.
- Create a backend email subscription route that calls the Brevo contacts API.
- Build an email signup component in the React frontend.
Sign up for Brevo and generate an API key
Brevo is an email marketing platform with a generous free tier of 300 emails per day. You will use its REST API to add visitor emails to a contact list without needing an SDK.
- Open your browser and go to brevo.com.
- Click the signup button and create a free account.
- Complete the account setup wizard (confirm your email, fill in your business name).
Now generate an API key so your Express server can authenticate with Brevo.
- Click your account dropdown in the top-right corner of the Brevo dashboard.
- Select Settings, then SMTP & API, then API Keys & MCP.
- Click Generate a new API key.
- Name the key something like MT5 Showcase.
- Click Generate, then click OK.
- Copy the API key immediately. It is only shown once.
Why copy it now?
Brevo only displays the full API key at the moment of creation. If you close the dialog without copying it, you will need to generate a new key.
Next, create a contact list where subscriber emails will be stored.
- In the Brevo sidebar, navigate to CRM, then Contacts, then Lists.
- Click Create a list.
- Name the list something like MT5 Showcase Subscribers.
- Select a folder (the default folder is fine).
- Click Create list.
- Note the list ID shown in the URL or list details. You will need it for your .env file.
Now add your real Brevo credentials to the server environment file.
- Switch back to VS Code and open server/.env.
- Replace the placeholder BREVO_API_KEY value with the API key you just copied.
- Replace the BREVO_LIST_ID value with the ID of the list you just created.
- Save server/.env.
Why not use a variable placeholder for the API key?
API keys are sensitive credentials. You should paste the real value directly into your .env file and never commit it to version control.
Can't find the API Keys page?
You can navigate directly to app.brevo.com/settings/keys/api in your browser while signed in to Brevo.
help me find the Brevo API key settings page
Create the email subscription API route
Your backend needs an endpoint that receives an email address from the frontend and forwards it to the Brevo contacts API. This follows the same proxy pattern you used for Myfxbook.
- Create a new file called email.js inside server/routes/ and add the following code:
const express = require('express');
const router = express.Router();
router.post('/subscribe', async (req, res) => {
const { email } = req.body;
// Validate that an email was provided
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
try {
// Call the Brevo contacts API to add the subscriber
const response = await fetch('https://api.brevo.com/v3/contacts', {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-key': process.env.BREVO_API_KEY,
},
body: JSON.stringify({
email: email,
listIds: [parseInt(process.env.BREVO_LIST_ID) || 2],
updateEnabled: true,
}),
});
// Handle successful creation (201) or no-content (204)
if (response.status === 201 || response.status === 204) {
return res.json({ message: 'Successfully subscribed!' });
}
// Handle duplicate email addresses
const data = await response.json();
if (data.code === 'duplicate_parameter') {
return res.json({ message: 'You are already subscribed!' });
}
return res.status(response.status).json({ error: data.message || 'Subscription failed' });
} catch (error) {
console.error('Error subscribing:', error.message);
res.status(500).json({ error: 'Subscription failed. Please try again.' });
}
});
module.exports = router;
What does this code do?
- The POST /subscribe route extracts the email from the request body and sends it to the Brevo contacts API at https://api.brevo.com/v3/contacts.
- The api-key header authenticates the request with your Brevo API key stored in the environment variable.
- The listIds field tells Brevo which contact list to add the subscriber to.
- The updateEnabled: true option prevents errors if the email already exists. It updates the contact instead.
- The duplicate_parameter check returns a friendly message when someone subscribes with an email that already exists.
Now wire the new route into the main Express server.
- Open server/index.js and find this line near the top:
const myfxbookRoutes = require('./routes/myfxbook');
- Add the email routes import directly below it, so the two lines look like this:
const myfxbookRoutes = require('./routes/myfxbook');
const emailRoutes = require('./routes/email');
- Then find this line further down:
app.use('/api', myfxbookRoutes);
- Add the email routes mount directly below it, so the two lines look like this:
app.use('/api', myfxbookRoutes);
app.use('/api', emailRoutes);
- Save server/index.js.
- Restart your Express server by pressing Ctrl+C in the server terminal, then running:
node index.js
- Test the new endpoint by opening a second terminal and running:
curl -X POST http://localhost:3001/api/subscribe -H "Content-Type: application/json" -d '{"email": "test@example.com"}'
- You should see a JSON response like {"message":"Successfully subscribed!"} in the terminal output.
Seeing a 500 error or subscription failed?
- Make sure you saved server/.env with your real Brevo API key, not the placeholder value.
- Check that BREVO_LIST_ID matches the list ID you created in the Brevo dashboard.
- Confirm you restarted the Express server after saving the .env file. Environment variables are only loaded at startup.
help me debug my Brevo API subscription endpoint returning 500 errors
✔️ Awesome, I've got everything!
Great. Double check you have saved both server/routes/email.js and server/index.js.
ⓧ I'd like to double check the full code
Here is the complete server/routes/email.js:
const express = require('express');
const router = express.Router();
router.post('/subscribe', async (req, res) => {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'Email is required' });
}
try {
const response = await fetch('https://api.brevo.com/v3/contacts', {
method: 'POST',
headers: {
'accept': 'application/json',
'content-type': 'application/json',
'api-key': process.env.BREVO_API_KEY,
},
body: JSON.stringify({
email: email,
listIds: [parseInt(process.env.BREVO_LIST_ID) || 2],
updateEnabled: true,
}),
});
if (response.status === 201 || response.status === 204) {
return res.json({ message: 'Successfully subscribed!' });
}
const data = await response.json();
if (data.code === 'duplicate_parameter') {
return res.json({ message: 'You are already subscribed!' });
}
return res.status(response.status).json({ error: data.message || 'Subscription failed' });
} catch (error) {
console.error('Error subscribing:', error.message);
res.status(500).json({ error: 'Subscription failed. Please try again.' });
}
});
module.exports = router;
Here is the complete server/index.js:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const myfxbookRoutes = require('./routes/myfxbook');
const emailRoutes = require('./routes/email');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.use('/api', myfxbookRoutes);
app.use('/api', emailRoutes);
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Build the email signup component
The backend is ready. Now you need a React component that collects an email address and sends it to your /api/subscribe endpoint.
- Create a new file called EmailSignup.jsx inside client/src/components/ and add the following code:
import { useState } from 'react';
function EmailSignup() {
// Track the email input, feedback message, and submission state
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage('');
try {
// Send the email to the backend subscribe route
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await response.json();
if (response.ok) {
setMessage(data.message);
setEmail('');
} else {
setMessage(data.error || 'Something went wrong.');
}
} catch {
setMessage('Network error. Please try again.');
} finally {
setSubmitting(false);
}
}
return (
<div className="email-signup">
<h2>Stay Updated</h2>
<p>Get notified about new robots, performance updates, and trading tips.</p>
<form onSubmit={handleSubmit} className="signup-form">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Subscribing...' : 'Subscribe'}
</button>
</form>
{message && <p className="signup-message">{message}</p>}
</div>
);
}
export default EmailSignup;
What does this component do?
- Three pieces of state manage the email input value, the feedback message, and whether a submission is in progress.
- The handleSubmit function sends a POST request to /api/subscribe with the email in the body. The Vite proxy forwards this to your Express server.
- On success, the form clears and shows the server's confirmation message. On failure, it displays the error.
- The button is disabled while submitting to prevent duplicate requests.
Now wire the component into your main app layout.
- Open client/src/App.jsx and find the existing imports at the top. Add the EmailSignup import so the import block looks like this:
import ProductCard from './components/ProductCard';
import PerformanceDashboard from './components/PerformanceDashboard';
import EmailSignup from './components/EmailSignup';
import products from './data/products';
import './App.css';
- Then find the closing </div> tag at the bottom of the App function (right after the performance section). Add the newsletter section above it so the end of your JSX looks like this:
<section className="section" id="performance">
<h2>Live Performance</h2>
<p>Real-time results from Myfxbook verified accounts.</p>
<PerformanceDashboard />
</section>
<section className="section" id="newsletter">
<EmailSignup />
</section>
</div>
- Save client/src/App.jsx.
Finally, add the styling for the email signup section.
- Open client/src/App.css and add the following styles at the bottom of the file:
.email-signup {
text-align: center;
padding: 3rem 2rem;
background-color: #1e293b;
border-radius: 12px;
border: 1px solid #334155;
}
.email-signup h2 {
color: #ffffff;
margin-bottom: 0.5rem;
}
.email-signup p {
color: #9ca3af;
margin-bottom: 1.5rem;
}
/* Flex layout for the email form */
.signup-form {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.signup-form input {
padding: 0.75rem 1rem;
border: 1px solid #334155;
border-radius: 8px;
background-color: #111827;
color: #e0e0e0;
font-size: 1rem;
width: 300px;
}
.signup-form input::placeholder {
color: #6b7280;
}
.signup-form button {
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.signup-form button:hover {
background-color: #059669;
}
/* Disabled state while submitting */
.signup-form button:disabled {
background-color: #374151;
cursor: not-allowed;
}
.signup-message {
margin-top: 1rem;
color: #10b981;
}
What do these styles do?
The .email-signup container gets the same dark card treatment as the product and performance cards. The form uses a flex layout to place the input and button side by side on wider screens and stack them on narrow screens.
The disabled button style (.signup-form button:disabled) gives clear visual feedback while a submission is in progress.
- Save client/src/App.css.
- Return to your browser at http://localhost:5173. You should see a "Stay Updated" section at the bottom of the page with an email input and a green "Subscribe" button.
- Enter a test email address and click Subscribe. You should see the message "Successfully subscribed!" appear below the form.
Not seeing the signup section or getting errors?
- Check that EmailSignup is imported in App.jsx and rendered inside the newsletter section.
- If the form submits but you see a network error, confirm your Express server is still running on port 3001 and that the Vite proxy in vite.config.js forwards /api to http://localhost:3001.
- If the section appears but the button does nothing, check the browser console for JavaScript errors.
help me debug my EmailSignup component not working
✔️ Awesome, I've got everything!
Great. Double check you have saved client/src/components/EmailSignup.jsx, client/src/App.jsx, and client/src/App.css.
ⓧ I'd like to double check the full code
Here is the complete client/src/components/EmailSignup.jsx:
import { useState } from 'react';
function EmailSignup() {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [submitting, setSubmitting] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage('');
try {
const response = await fetch('/api/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await response.json();
if (response.ok) {
setMessage(data.message);
setEmail('');
} else {
setMessage(data.error || 'Something went wrong.');
}
} catch {
setMessage('Network error. Please try again.');
} finally {
setSubmitting(false);
}
}
return (
<div className="email-signup">
<h2>Stay Updated</h2>
<p>Get notified about new robots, performance updates, and trading tips.</p>
<form onSubmit={handleSubmit} className="signup-form">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={submitting}>
{submitting ? 'Subscribing...' : 'Subscribe'}
</button>
</form>
{message && <p className="signup-message">{message}</p>}
</div>
);
}
export default EmailSignup;
Here is the complete client/src/App.jsx:
import ProductCard from './components/ProductCard';
import PerformanceDashboard from './components/PerformanceDashboard';
import EmailSignup from './components/EmailSignup';
import products from './data/products';
import './App.css';
function App() {
return (
<div className="app">
<nav className="navbar">
<h1 className="brand">MT5 Trading Robots</h1>
</nav>
<section className="hero">
<h2>Automated Trading Solutions</h2>
<p>Professional MT5 trailing robots backed by verified live performance.</p>
</section>
<section className="section" id="products">
<h2>Our Robots</h2>
<div className="products-grid">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</section>
<section className="section" id="performance">
<h2>Live Performance</h2>
<p>Real-time results from Myfxbook verified accounts.</p>
<PerformanceDashboard />
</section>
<section className="section" id="newsletter">
<EmailSignup />
</section>
</div>
);
}
export default App;
Here is the complete client/src/App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0a0e17;
color: #e0e0e0;
}
.app {
min-height: 100vh;
}
.navbar {
display: flex;
align-items: center;
padding: 1rem 2rem;
background-color: #111827;
border-bottom: 1px solid #1f2937;
}
.brand {
font-size: 1.5rem;
color: #10b981;
}
.hero {
text-align: center;
padding: 4rem 2rem;
background: linear-gradient(135deg, #111827 0%, #1e293b 100%);
}
.hero h2 {
font-size: 2.5rem;
margin-bottom: 1rem;
color: #ffffff;
}
.hero p {
font-size: 1.2rem;
color: #9ca3af;
}
.section {
padding: 3rem 2rem;
max-width: 1200px;
margin: 0 auto;
}
.section h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
color: #ffffff;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.product-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.product-card h3 {
color: #10b981;
margin-bottom: 0.75rem;
}
.product-card p {
color: #9ca3af;
margin-bottom: 1rem;
line-height: 1.6;
}
.features-list {
list-style: none;
margin-bottom: 1.5rem;
}
.features-list li {
padding: 0.25rem 0;
color: #d1d5db;
}
.features-list li::before {
content: '+ ';
color: #10b981;
}
.buy-button {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s;
}
.buy-button:hover {
background-color: #059669;
}
.performance-dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.performance-card {
background-color: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 1.5rem;
}
.performance-card h3 {
color: #ffffff;
margin-bottom: 1rem;
}
.metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.metric {
display: flex;
flex-direction: column;
}
.metric-label {
font-size: 0.85rem;
color: #9ca3af;
}
.metric-value {
font-size: 1.4rem;
font-weight: 700;
color: #e0e0e0;
}
.metric-value.gain {
color: #10b981;
}
.metric-value.drawdown {
color: #ef4444;
}
.loading,
.error {
text-align: center;
padding: 2rem;
color: #9ca3af;
}
.email-signup {
text-align: center;
padding: 3rem 2rem;
background-color: #1e293b;
border-radius: 12px;
border: 1px solid #334155;
}
.email-signup h2 {
color: #ffffff;
margin-bottom: 0.5rem;
}
.email-signup p {
color: #9ca3af;
margin-bottom: 1.5rem;
}
.signup-form {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.signup-form input {
padding: 0.75rem 1rem;
border: 1px solid #334155;
border-radius: 8px;
background-color: #111827;
color: #e0e0e0;
font-size: 1rem;
width: 300px;
}
.signup-form input::placeholder {
color: #6b7280;
}
.signup-form button {
padding: 0.75rem 1.5rem;
background-color: #10b981;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.signup-form button:hover {
background-color: #059669;
}
.signup-form button:disabled {
background-color: #374151;
cursor: not-allowed;
}
.signup-message {
margin-top: 1rem;
color: #10b981;
}
Your email capture is working end-to-end. Visitors can now subscribe to your marketing list directly from your showcase site. Next up, you will tackle the Secret Mission and add a custom robot services request form.
Secret mission
Add a Custom Robot Services Request Form
What if visitors could request a custom trading robot built just for them? Add a multi-field services request form with client-side validation, a new backend endpoint, and Brevo transactional email notifications sent straight to your inbox.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with free-tier services, so there are no ongoing costs.
Resources you used:
- Running development servers (React on port 5173 and Express on port 3001).
- Project folder on your Desktop (the folder containing the client/ and server/ directories).
- Brevo API key (active in your Brevo account at app.brevo.com/settings/keys/api).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project further.
- Your React dev server and Express server continue running in their terminal windows.
- Your Brevo account stays active on the free tier (300 emails per day, no charge).
- Your Myfxbook credentials remain stored in server/.env locally.
Pause - I'll come back to this later
Stop the running servers to free up terminal sessions and memory. Your project files stay on disk so you can pick up where you left off.
- Switch to the terminal running your React dev server and press Ctrl+C to stop it.
- Switch to the terminal running your Express server and press Ctrl+C to stop it.
- To restart later, open two terminal windows. In one, navigate to your server/ directory and run node index.js. In the other, navigate to your client/ directory and run npm run dev.
Delete - I don't want to use this again
Remove all project files and revoke your API key. This is a complete teardown.
- Stop both running servers by pressing Ctrl+C in each terminal window.
- Delete your project folder by running the following command (macOS/Linux):
cd ~/Desktop
rm -rf mt5-showcase
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q mt5-showcase
Use your actual folder name
Replace mt5-showcase with whatever you named your project folder on the Desktop.
- Confirm the folder is gone by running:
ls
Your project folder should no longer appear in the output.
- Revoke your Brevo API key. Open app.brevo.com/settings/keys/api in your browser.
- Find the API key you created for this project and delete it.
- If you added test contacts to your Brevo list during the project, you can remove them from the Contacts page in your Brevo dashboard.
That's a Wrap!
That's a Wrap!
Nice work! You just built a full-stack business showcase website for your MT5 trading robots, complete with live performance data, product cards, and email marketing integration.
You've learned how to:
- Build a full-stack React + Express.js web application using the Backend-for-Frontend (BFF) proxy pattern to securely access the Myfxbook API without exposing credentials to the browser.
- Display live trading performance data (gain, drawdown, daily/monthly stats) fetched from Myfxbook, and create product showcase cards with direct MQL5 Market purchase links.
- Integrate the Brevo email marketing REST API to capture visitor emails and add them to a contact list for marketing campaigns.
- Secret Mission: Added a custom robot services request form with client-side validation and Brevo transactional email notifications.
Ready to quiz yourself?