What is MVVM? A Complete Guide to the Model-View-ViewModel Architecture
MVVM intro
For complex applications, software architecture plays a critical role in maintainability and scalability. One architectural pattern that stands out—especially in UI-heavy applications—is MVVM (Model-View-ViewModel). Whether you’re building mobile apps, web frontends, or cross-platform solutions, MVVM offers a clean way to separate concerns and streamline your codebase. In this post, we’ll break down what MVVM is, why it matters, and how you can start using it effectively in your projects.
Understanding the Basics of MVVM
What is MVVM?
MVVM stands for Model-View-ViewModel, a software design pattern that encourages a clear separation between the UI and business logic. Originating in Microsoft’s WPF framework, MVVM has since become widely adopted in various development ecosystems.
Components Breakdown:
- Model: Represents the data and business logic.
- View: The user interface—the what the user sees and interacts with.
- ViewModel: Sits between the Model and View. It exposes data streams and commands to the View and translates user actions into operations on the Model.
Get the complete code from my github
Why Choose MVVM?
- Separation of Concerns: MVVM forces a clean separation between UI and logic, reducing tight coupling and spaghetti code.
- Improved Unit testing: Because ViewModels are independent of the UI, they’re easy to unit test.
- Maintainability: Components are modular and reusable, which is ideal for growing teams and evolving features.
MVVM in Action (Example)
Imagine a simple weather app:
- The Model fetches temperature data from an API.
- The ViewModel exposes a temperature observable and a loadWeather() command.
- The View binds to the temperature and calls loadWeather() when the user taps a button.
- With proper data binding, the UI updates automatically when the data changes—no manual DOM manipulation or state juggling. (not in this example)
Code sample (NodeJS)
Model:
class WheaterModel {
constructor(city, temp, condition) {
this.city = city;
this.temp = temp;
this.condition = condition;
}
}
module.exports = WheaterModel;
View:
const weatherViewModel = require('../viewmodels/WeatherViewModel');
// Get the request from the frontend (city) to fetch the weather and send it back as a JSON response
async function renderWeather(req, res) {
try {
// This allows the user to specify a city in the URL, e.g., /weather?city=London
// If no city is specified, it defaults to 'Brussel
const city = req.query.city || 'Brussel';
const weatherData = await weatherViewModel.getWeatherData(city);
res.json(weatherData);
}
catch (err) {
res.status(500).json({ error: 'Failed to fetch weather data' });
}
}
module.exports = { renderWeather };
ViewModel:
const WeatherService = require('../services/WeatherService');
const WeatherModel = require('../models/WeatherModel');
class WeatherViewModel {
// This function fetches the weather data for a given city
// It uses the WeatherService to get the data and then formats it into a more user-friendly structure
// The data is then returned to the view for rendering
// Note: The city parameter is expected to be a string representing the city name
async getWeatherData(city) {
const data = await WeatherService.fetchWeather(city);
// Create a new WeatherModel instance with the fetched data
// This model encapsulates the weather data in a structured format
// The WeatherModel constructor takes the city name, temperature, and weather condition as parameters
const weather = new WeatherModel(
data.name,
data.main.temp,
data.weather[0].description
);
// Return a simplified object containing the city, temperature, and weather condition
// This is the data that will be sent back to the view for rendering
// The temperature is formatted to include the degree symbol and the condition is converted to uppercase for better readability
// This makes it easier for the view to display the weather information without needing to know the details of the WeatherModel
return {
city: weather.city,
temperature: `${weather.temp}°C`,
summary: weather.condition.toUpperCase()
};
}
}
Service:
const axios = require('axios');
require('dotenv').config();
class WeatherService {
// We use async/await to make this asynchronous function
// so we dont block the application/the users does not need to wait
async fetchWeather(city) {
// OpenWeather API URL, set wheater variable from model and get API key from .env file
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${process.env.WHEATER_API_KEY}`;
// Try/Catch block (always catch errors!), get the weather for given city trough the url variable
// and return the data
// If an error occurs, log it to the console and throw a new error with a custom message
// This will be caught in the viewmodel and handled accordingly
// Note: The API key is stored in the .env file for security reasons
try {
const response = await axios.get(url);
const data = response.data;
const res = await axios.get(url);
return res.data;
}
catch (error) {
console.error('Error fetching weather data:', error);
throw new Error('Could not fetch weather data');
}
}
}
module.exports = new WeatherService();
Routes:
const express = require('express');
const { renderWeather } = require('../views/WeatherView');
// Initialize the router
const router = express.Router();
// Define the route for fetching weather data
// (from the file in the views folder)
router.get('/', renderWeather);
module.exports = router;
App.js:
const express = require('express');
const weatherRoutes = require('./routes/Weather');
// Initialize path so we canuse the frontend .html file
const path = require('path');
// Load environment variables from .env file
// This is important for accessing the API key securely
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// Set the view engine to EJS (or any other template engine you prefer) here we use a plain HTML file.
app.use(express.static(path.join(__dirname, '../public')));
// This is an API backend so we say it needs to use JSON
app.use(express.json());
// The routes for the weather API
// This will handle requests to /weather and forward them to the WeatherView
app.use('/weather', weatherRoutes);
app.get('/', (req, res) => {
res.send('App could not load properly, please check the console for errors.');
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Index.html
<!DOCTYPE html>
<html>
<head>
<title>Weather App</title>
</head>
<body>
<h1>Weather Info</h1>
<form id="weatherForm">
<input type="text" id="city" placeholder="Enter city" />
<button type="submit">Get Weather</button>
</form>
<div id="result"></div>
<script>
document.getElementById('weatherForm').addEventListener('submit', async (e) => {
e.preventDefault();
const city = document.getElementById('city').value;
const res = await fetch(`/weather?city=${city}`);
const data = await res.json();
document.getElementById('result').innerHTML = `
<h2>${data.city}</h2>
<p>${data.temperature}</p>
<p>${data.summary}</p>
`;
});
</script>
</body>
</html>
You can get a free API key from: https://openweathermap.org/
Place it in the .env file (never upload the .env file to github for security, this is just a demo so i did upload it)
Get the complete code from my Github
Frameworks That Use MVVM
- Android (Jetpack + Kotlin): MVVM is the recommended architecture.
- iOS (SwiftUI): Heavily encourages MVVM through declarative bindings.
- Vue.js: Not explicitly MVVM, but follows similar principles with reactive bindings.
- .NET MAUI/Xamarin: MVVM is baked in with tooling and conventions.
- Knockout.js: One of the original MVVM frameworks for the web.
Common Pitfalls
- Fat ViewModels: Don’t let your ViewModel become a dumping ground for every concern.
- make use of service files that do one thing and one thing only to eliminate spaghetti code.
- UI logic leakage: Avoid putting UI-specific logic in the ViewModel.
- Over-architecture: MVVM can be overkill for very small projects—stick to the MVC architecture for those projects.
For my code example i recommend to use MVC intead of mvvm because its a small program, but for the sake of a demo it will do.
Best Practices
- Use reactive patterns (RxJS, LiveData, Combine) for clean data flow.
- Keep ViewModel pure—no direct references to UI frameworks.
- Leverage dependency injection to isolate business logic and the use of services that do one thing and one thing only.