What is an MVP in Software? Build One with a Free API

1 views

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.

bash
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

bash
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)

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

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

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:

bash
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:

js
// 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 current or hourly fields.
  • Same city name in several countries: count=1 takes 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:

js
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 Map with 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.

Open the playlist on YouTube · CheatCoders on YouTube

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.

Subscribe free →


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.