A weather app is a compact full-stack project: a React page asks your own Node.js and Express API for a city's weather, the API calls a weather service, and MySQL caches the answer. This guide builds it with current tools (Vite, Express 5, mysql2) and one rule kept throughout: the weather API key stays on the server.
Build the front end with Vite and React, and call only your own /api/weather route. The Express server holds the weather API key in an environment variable, validates the city name, calls the provider, and caches each result in MySQL for ten minutes using parameterised queries. For production, build the React app and let Express serve it. On Domain India cPanel or DirectAdmin hosting, run it with the panel's Node.js tool and set the key in its environment variables.
1. How the app fits together
- Browser.The React page sends
GET /api/weather?city=Puneto your own server. It never talks to the weather provider and never sees the key. - Express.Validates the city name, then looks for a fresh cached answer in MySQL.
- Weather provider.On a cache miss, Express calls the provider with the secret key.
- MySQL.Stores the answer, so repeat searches are fast and you stay inside the provider's free quota.
Anything bundled into the front end, including variables Vite exposes with the VITE_ prefix, is readable by every visitor. A key there can be copied and your quota used up. Keep it in the server's environment only.
2. Set up the project
You need a current Node.js LTS release (24 at the time of writing), a MySQL or MariaDB database, and a free API key from a weather provider. This guide uses OpenWeatherMap's current weather endpoint; any provider with a similar JSON API works the same way.
mkdir weather-app && cd weather-app
npm init -y && npm pkg set type=module
npm install express mysql2Create a .env file and add it to .gitignore:
WEATHER_API_KEY=your-key-here
DB_HOST=localhost
DB_USER=weather_user
DB_PASSWORD=a-strong-password
DB_NAME=weather_dbNode.js reads it directly with node --env-file=.env server.js, so the dotenv package is optional.
3. Create the cache table
Run this once, in phpMyAdmin or the MySQL command line:
CREATE TABLE weather_cache (
city VARCHAR(80) NOT NULL PRIMARY KEY,
data TEXT NOT NULL,
fetched_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);The JSON response is stored as text, which works the same on MySQL and MariaDB. A migration tool is not needed for one table.
4. Build the Express API
// server.js
import express from 'express';
import mysql from 'mysql2/promise';
import path from 'node:path';
const app = express();
const db = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 5,
});
const CITY = /^[\p{L} .'-]{2,60}$/u;
app.get('/api/weather', async (req, res) => {
const city = String(req.query.city ?? '').trim();
if (!CITY.test(city)) return res.status(400).json({ error: 'Enter a valid city name' });
const key = city.toLowerCase();
const [rows] = await db.execute(
'SELECT data FROM weather_cache WHERE city = ? AND fetched_at > NOW() - INTERVAL 10 MINUTE',
[key],
);
if (rows.length) return res.json(JSON.parse(rows[0].data));
const url = new URL('https://api.openweathermap.org/data/2.5/weather');
url.search = new URLSearchParams({ q: city, units: 'metric', appid: process.env.WEATHER_API_KEY });
const r = await fetch(url, { signal: AbortSignal.timeout(8000) });
if (r.status === 404) return res.status(404).json({ error: 'City not found' });
if (!r.ok) return res.status(502).json({ error: 'Weather service unavailable' });
const w = await r.json();
const result = {
city: w.name,
country: w.sys?.country,
tempC: Math.round(w.main.temp),
feelsLikeC: Math.round(w.main.feels_like),
humidity: w.main.humidity,
description: w.weather?.[0]?.description,
};
await db.execute(
`INSERT INTO weather_cache (city, data) VALUES (?, ?)
ON DUPLICATE KEY UPDATE data = VALUES(data), fetched_at = CURRENT_TIMESTAMP`,
[key, JSON.stringify(result)],
);
res.json(result);
});
// Serve the built React app in production
const dist = path.resolve('client/dist');
app.use(express.static(dist));
app.get('/{*splat}', (req, res) => res.sendFile(path.join(dist, 'index.html')));
app.listen(process.env.PORT || 3001);What makes this safe and current:
- Parameterised queries (
?placeholders withexecute) make SQL injection impossible through the city name. - Input validation accepts letters in any language, spaces, dots, apostrophes and hyphens only.
- Only the fields you need are returned, so the provider's full response and your key never reach the browser.
- Node's built-in
fetchreplacesaxioson the server, with a timeout so a slow provider cannot hang requests. - Express 5 passes errors from
asynchandlers to its error handler automatically, and uses the/{*splat}syntax for catch-all routes.
Test it with node --env-file=.env server.js, then open http://localhost:3001/api/weather?city=Mumbai.
5. Build the React front end with Vite
Create React App is deprecated; use Vite:
npm create vite@latest client -- --template react
cd client && npm installIn client/vite.config.js, add server: { proxy: { '/api': 'http://localhost:3001' } } so that /api calls reach Express during development.
// client/src/App.jsx
import { useState } from 'react';
export default function App() {
const [city, setCity] = useState('');
const [weather, setWeather] = useState(null);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function search(e) {
e.preventDefault();
setLoading(true); setError(''); setWeather(null);
try {
const r = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const body = await r.json();
if (!r.ok) throw new Error(body.error || 'Something went wrong');
setWeather(body);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
return (
<main>
<h1>Weather</h1>
<form onSubmit={search}>
<label htmlFor="city">City</label>
<input id="city" value={city} onChange={(e) => setCity(e.target.value)} required />
<button disabled={loading}>{loading ? 'Loading…' : 'Search'}</button>
</form>
{error && <p role="alert">{error}</p>}
{weather && (
<section aria-live="polite">
<h2>{weather.city}, {weather.country}</h2>
<p>{weather.tempC}°C, feels like {weather.feelsLikeC}°C</p>
<p>{weather.description} · Humidity {weather.humidity}%</p>
</section>
)}
</main>
);
}Run npm run dev in client and the server in another terminal. When it works, run npm run build in client to produce client/dist, which Express serves.
6. Prepare for production
- Add
"start": "node server.js"to the scripts in the rootpackage.json. - Set every variable from
.envin your host's environment settings rather than uploading the file. If you must upload it, keep it outside the public web folder. - Build the React app on your own computer and upload
client/dist, so the server does not need to run the build. - Handle provider rate limits: the ten-minute cache already cuts repeat calls sharply.
7. Running this on Domain India
| Where | How it runs | Database |
|---|---|---|
| cPanel or DirectAdmin shared hosting | The panel's Node.js tool (Setup Node.js App on cPanel) | MySQL, created in the panel |
| App Platform | Node.js detected and built automatically | PostgreSQL included, not MySQL |
| VPS | You install and manage everything; no cPanel | Whatever you install |
Shared hosting fits this app well. In cPanel, open Setup Node.js App, create an application with the startup file server.js, upload your files (without node_modules), click Run NPM Install and start the app. Put WEATHER_API_KEY and the database settings in the app's Environment variables section, not in a file under public_html. When we checked on 23 September 2026, cPanel offered Node.js 20, 22 and 24; choose 22 or 24. The step-by-step guide is How to deploy a Node.js app on shared hosting, and database setup is in How to connect to your MySQL database. cPanel can serve a cached copy of a page for up to 120 minutes, so add &t=1 to a URL when testing a change. If the app cannot reach the weather service, ask support to check outbound access from your account.
App Platform builds Node.js apps automatically, but its included database is PostgreSQL. Switch mysql2 to the pg package and adjust the SQL (for example ON CONFLICT (city) DO UPDATE), and read the connection string from DATABASE_URL. See Getting started with the App Platform.
- 25 GB NVMe SSD Storage
- 50 GB Monthly Bandwidth
- 1 Website
- 10 Email Accounts
- 512 MB RAM per app
- 1 vCPU
- 5 GB NVMe SSD
- PostgreSQL Database
Where should I keep the weather API key in a React app?
On the server only. Put it in an environment variable read by Express, and have React call your own API route. Anything bundled into the React build, including VITE_ variables, is visible to every visitor.
Why cache weather data in MySQL?
Weather changes slowly, so a ten-minute cache makes repeat searches fast and keeps you within the weather provider's free request quota.
Do I still need Sequelize for this app?
No. One table and two queries are simple with mysql2 and parameterised queries. An ORM or migration tool helps once you have many tables.
Is Create React App still recommended?
No. Create React App is deprecated. Use Vite, or a framework, to start new React projects.
Can I run a Node.js and MySQL app on Domain India shared hosting?
Yes. cPanel and DirectAdmin have a Node.js application tool in the control panel, and you create the MySQL database in the same panel. Set your API key and database password as environment variables for the app.
Can I use MySQL on the App Platform?
The App Platform includes a PostgreSQL database. To use it, switch the app from mysql2 to the pg package and adjust the SQL.
Ready to put your app online? Start with cPanel hosting, look at the App Platform, or ask us in a support ticket which option fits.
Run Node.js apps from the control panel, with MySQL databases and free SSL.
See cPanel plans