Two acronyms get mixed up all the time in beginner tech conversations: MVP and API. One is a whole product strategy, the other is a piece of plumbing. This tutorial sorts out the difference, then builds a real MVP that uses two APIs: “Will it rain?”, a tiny web app where you type a city and get a yes or no on whether to take an umbrella today. It’s about a hundred lines: a small Node.js + Express backend and a plain HTML/JS frontend, using the free Open-Meteo APIs with no key and no sign-up.
⚡ TL;DR – MVP (minimum viable product): the smallest real product you can put in front of people to answer one question: does anyone want this? – API (application programming interface): how one program talks to another, with requests and responses in an agreed format. – An API is a part. An MVP is a whole product, and it can use APIs. – Scope ruthlessly: one input, one answer. No accounts, no settings, no dark mode. – Proxy external APIs through your own endpoint and return clear errors (400, 404, 502).
Before you start
- Node.js 18 or newer (for built-in
fetch). - Basic HTML and JavaScript.
- A terminal and a browser. No API keys. Open-Meteo’s free API is for non-commercial use, rate-limited (under 10,000 calls a day), and licensed CC BY 4.0, so credit “Weather data by Open-Meteo.com”.
Step 1: Understand what an MVP is
Minimum means you cut everything that isn’t needed to answer your one question. Viable means it still has to work: a broken prototype tells you nothing, because people leave before they reach the value.
The idea being tested here: most weather apps show a wall of numbers, but people just want “Do I need an umbrella today?” The MVP is one input box and one answer. If people use it and come back, the idea has legs and you add features. If nobody cares, you’ve lost a weekend, not six months.
Step 2: Understand what an API is
Open the Open-Meteo forecast API in a browser with a latitude, longitude and the fields you want, and it returns JSON. No button, no design, no user in mind: it’s built for code to read.
curl "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&daily=precipitation_probability_max&timezone=auto&forecast_days=1"
Open-Meteo also has a geocoding API that turns a city name into coordinates. Our app chains the two.
Step 3: Plan the scope on paper
| In the MVP | Not in the MVP |
|---|---|
| Type a city, get yes/no | Accounts, saved cities |
| Today’s rain chance and amount | Hourly charts, maps |
| Clear error for unknown cities | Notifications, dark mode |
Write down the rule for “yes”: for example, a rain probability of 50% or more, or any expected rainfall. Being explicit makes the app testable.
Step 4: Set up the project
mkdir will-it-rain && cd will-it-rain
npm init -y
npm install express
mkdir public
Add "type": "module" and a start script ("start": "node server.js") to package.json.
Step 5: Build the backend (server.js)
import express from 'express';
const app = express();
app.use(express.static('public'));
app.get('/api/rain', async (req, res) => {
const city = String(req.query.city ?? '').trim();
if (!city) return res.status(400).json({ error: 'Please enter a city.' });
try {
const geo = await (await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`)).json();
const place = geo.results?.[0];
if (!place) return res.status(404).json({ error: `Couldn't find "${city}".` });
const q = new URLSearchParams({
latitude: place.latitude, longitude: place.longitude, timezone: 'auto', forecast_days: 1,
daily: 'precipitation_probability_max,precipitation_sum',
});
const r = await fetch(`https://api.open-meteo.com/v1/forecast?${q}`);
if (!r.ok) throw new Error(`forecast ${r.status}`);
const { daily } = await r.json();
const chance = daily.precipitation_probability_max[0];
const mm = daily.precipitation_sum[0];
res.json({ place: `${place.name}, ${place.country}`, chance, mm, umbrella: chance >= 50 || mm > 0 });
} catch (e) {
console.error(e);
res.status(502).json({ error: 'Weather service unavailable. Try again shortly.' });
}
});
app.listen(3000, () => console.log('http://localhost:3000'));
Why proxy through your own endpoint instead of calling Open-Meteo from the browser?
- The frontend gets one simple response shape, whatever happens upstream.
- You can swap providers, add caching or add a key later without touching the frontend.
- Errors are mapped to clear status codes: 400 for bad input, 404 for an unknown city, 502 when the upstream service fails.
Step 6: Build the frontend HTML
<!-- public/index.html -->
<main>
<h1>Will it rain?</h1>
<form id="f">
<input id="city" placeholder="City" required>
<button>Check</button>
</form>
<p id="out" aria-live="polite"></p>
<small>Weather data by <a href="https://open-meteo.com/">Open-Meteo.com</a></small>
</main>
<script type="module" src="app.js"></script>
Step 7: Write app.js
const form = document.querySelector('#f');
const out = document.querySelector('#out');
form.addEventListener('submit', async e => {
e.preventDefault();
out.textContent = 'Checking…';
const city = document.querySelector('#city').value;
try {
const r = await fetch(`/api/rain?city=${encodeURIComponent(city)}`);
const data = await r.json();
if (!r.ok) { out.textContent = data.error; return; }
out.textContent = data.umbrella
? `Yes, take an umbrella. ${data.place}: ${data.chance}% chance, ${data.mm} mm.`
: `No umbrella needed in ${data.place} today.`;
} catch {
out.textContent = 'Network error. Are you offline?';
}
});
textContent, not innerHTML, so a city name can never inject markup.
Step 8: Style it, run it, test it
A few lines of CSS (centered layout, large input, big answer text) are enough. Then:
npm start
curl "localhost:3000/api/rain?city=London"
curl -i "localhost:3000/api/rain?city=" # 400
curl -i "localhost:3000/api/rain?city=Qwxyzzz" # 404
Test the 502 path by temporarily breaking the forecast URL. Every error path should produce a sentence a user understands.
Step 9: Deploy (briefly)
Any Node host works: Render, Railway, Fly.io or a small VPS. Read the port from process.env.PORT ?? 3000. Before sharing widely, add a short cache (rain forecasts don’t change every second) so you stay well inside the free API’s limits.
Step 10: Measure whether people want it
An MVP that nobody measures is just a small app. Decide up front what “people want this” means, then collect the minimum data to tell. You don’t need an analytics platform; a log line per request is enough at this stage:
// in /api/rain, after a successful lookup
console.log(JSON.stringify({ at: new Date().toISOString(), city: place.name, umbrella: chance >= 50 || mm > 0 }));
From that log you can answer the questions that matter: how many checks a day, how many distinct cities, and whether the same people come back the next morning (add an anonymous cookie id if you need repeat visits). Pair it with a single feedback link on the page (“Was this right?”). Write down your threshold before launch, for example “people check at least three mornings in their first week”, so the result is a decision, not a debate.
MVP vs API at a glance
| MVP | API | |
|---|---|---|
| What it is | A whole product, minimal | An interface between programs |
| Who uses it | People | Code |
| Main question | Does anyone want this? | How do I get this data or action? |
| In this project | “Will it rain?” app | Open-Meteo geocoding and forecast, plus our own /api/rain |
Notice the last row: the app contains an API of its own. /api/rain is an API our frontend calls. That’s why the two terms get confused, and why the distinction is about purpose, not technology.
Troubleshooting
fetch is not defined: Node is older than 18. Upgrade.- “Cannot use import statement outside a module”: add
"type": "module". - Answer says no rain while it’s raining: the daily maximum probability describes the whole day. For “right now”, use Open-Meteo’s
currentorhourlyfields. - Same city name in several countries:
count=1takes the top match. Show the resolved place so users can spot it, and consider “City, Country” input later.
Best practices for MVPs
- Decide the success signal before building (return visits, shares, sign-ups for updates).
- Ship the smallest thing that works end to end.
- Handle errors properly; viability includes failure cases.
- Note what you deliberately left out, so the next step is a decision rather than a pile of features.
Edge cases your MVP should still handle
An MVP is small, not fragile. A few cheap checks prevent the failures that make early users give up:
app.get('/api/rain', async (req, res) => {
const city = String(req.query.city ?? '').trim();
if (city.length < 2 || city.length > 80) return res.status(400).json({ error: 'Enter a city name' });
try {
// ...geocode + forecast as above, with AbortSignal.timeout(8000) on each fetch
} catch (e) {
console.error(e);
res.status(502).json({ error: 'Weather service is unavailable, try again shortly' });
}
});
- Ambiguous names. “Springfield” matches many places. Show the country and region from the geocoding result so users can tell which one they got.
- Slow upstream. Put a timeout on every fetch, and show a loading state in the UI.
- Repeated lookups. A tiny in-memory cache (a
Mapwith a 10-minute expiry) cuts API calls and makes the app feel instant.
FAQ
Is an MVP just a prototype?
No. A prototype can be fake. An MVP is real and used by real people.
Can an API be an MVP?
Yes, if the product is the API itself and the customers are developers. Then the “minimum” is the smallest useful set of endpoints.
When should I add features?
When usage tells you which ones matter.
Official documentation
Key takeaways
- An MVP tests one question with the smallest working product.
- An API is how programs talk; MVPs often use them.
- Proxy upstream APIs and map failures to clear status codes.
- Cut features, never cut working error handling.
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
PDF for this post is being prepared. Subscribe now and we’ll unlock downloads sitewide as packs go live.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.