A simple weather dashboard built with Symfony 8 and the OpenWeatherMap API.
Created by Jatniel Guzmán - jatniel.dev
Built for the DevChallenges - Week 15 organized by YoanDev.
Live demo (temporary): meteodash.jatniel.dev
- Search weather by city name
- Current temperature, description and weather icon
- Detail cards: humidity, wind speed, feels like, pressure
- 3-hour forecast (next 5 slots)
- Fully responsive: mobile, tablet, Full HD, 2K, 4K, 5K, 6K
- No external JS dependencies - vanilla JavaScript
The user types a city name. The frontend sends an AJAX request to the Symfony backend, which calls the OpenWeatherMap API and returns weather data as JSON.
Browser (weather.js)
→ GET /api/weather/{city}
→ WeatherController::apiWeather()
→ WeatherService::getWeather() → OpenWeatherMap /data/2.5/weather
→ WeatherService::getForecast() → OpenWeatherMap /data/2.5/forecast
← JSON { current: {...}, forecast: [...] }
← DOM update
Renders the weather dashboard page.
Returns current weather and forecast as JSON.
Example response:
{
"current": {
"city": "Madrid",
"country": "ES",
"temperature": 24.5,
"feels_like": 23.8,
"description": "partly cloudy",
"icon": "02d",
"humidity": 62,
"wind_speed": 5.1,
"wind_direction": "NW",
"visibility": 10000,
"pressure": 1018
},
"forecast": [
{
"time": "15:00",
"temperature": 26.0,
"description": "clear sky",
"icon": "01d"
}
]
}Error response:
{ "error": "Impossible de récupérer la météo pour cette ville." }meteodash/
├── assets/
│ ├── app.js # JS entry point
│ ├── weather.js # AJAX logic & DOM updates
│ └── styles/
│ ├── app.css # Global layout, CSS variables, responsive
│ └── weather.css # Dashboard component styles
├── config/
│ ├── packages/ # Symfony bundle configs
│ └── services.yaml # Service autowiring
├── public/
│ └── index.php # Front controller
├── src/
│ ├── Controller/
│ │ └── WeatherController.php # Routes: / and /api/weather/{city}
│ ├── DTO/
│ │ ├── WeatherData.php # Current weather value object
│ │ └── HourlyForecastData.php# Forecast entry value object
│ └── Service/
│ └── WeatherService.php # OpenWeatherMap API client
├── templates/
│ ├── base.html.twig # Base HTML layout
│ └── weather/
│ └── index.html.twig # Dashboard page
├── .env # Environment variables
├── composer.json
└── importmap.php # Asset Mapper config
| Choice | Why |
|---|---|
final readonly DTOs |
Immutability - data cannot be altered after creation |
JsonSerializable on DTOs |
The DTO controls its own JSON shape, keeping the controller thin |
#[Autowire(env:)] |
Self-describing service - no manual wiring in services.yaml |
| Constructor injection | Explicit dependencies, easier to test |
ExceptionInterface catch |
Only catches HTTP client errors - unexpected bugs are not silently swallowed |
| Asset Mapper | Symfony-native asset management - no Node.js, no Webpack, no build step |
| CSS custom properties | Single source for colors and sizing, easy to maintain |
em units in components |
All elements scale proportionally with the base font size |
- PHP 8.4+
- Composer
- Symfony CLI (optional, for local server)
git clone <repository-url>
cd meteodash
composer installThis app uses the OpenWeatherMap API. You need a free API key:
- Create an account at openweathermap.org
- Go to API keys in your profile and copy your key
- Create a
.env.localfile at the project root:
OPENWEATHERMAP_API_KEY=your_api_key_heresymfony server:startThen open http://localhost:8000.
- PHP 8.4+
- Composer
- Apache with
mod_rewriteenabled
1. Clone and install dependencies (no dev)
git clone <repository-url>
cd meteodash
composer install --no-dev --optimize-autoloader2. Create .env.local at the project root with your real values:
APP_ENV=prod
APP_SECRET=your_secret_here # generate with: openssl rand -hex 32
OPENWEATHERMAP_API_KEY=your_key_here
WEATHER_LANG=fr
WEATHER_UNITS=metric3. Compile assets
php bin/console asset-map:compileThis dumps all CSS/JS into public/assets/ as static files.
4. Warm up the cache
php bin/console cache:clear --env=prod5. Set the document root to the public/ folder in your hosting control panel.
6. Apache .htaccess
The file public/.htaccess (included via symfony/apache-pack) handles URL rewriting. Make sure Apache has AllowOverride All enabled for the directory, otherwise routing will not work.
- Never commit
.env.local— it contains secrets. - The
var/directory must be writable by the web server. - Weather results are cached for 10 minutes using the filesystem cache (
var/cache/).
5 tests, 23 assertions - no API key needed to run them.
vendor/bin/phpunit| Test | Type | What it verifies |
|---|---|---|
WeatherServiceTest::testGetWeatherReturnsCorrectDto |
Unit | API response → WeatherData mapping (all fields) |
WeatherServiceTest::testGetForecastReturnsLimitedEntries |
Unit | API response → HourlyForecastData[] with correct limit and city-local times |
WeatherControllerTest::testHomePageRendersSuccessfully |
Functional | GET / returns 200 and renders the page |
WeatherControllerTest::testApiRejectsTooShortCity |
Functional | GET /api/weather/a returns 400 with error message |
WeatherControllerTest::testApiReturnsNotFoundWhenCityIsUnknown |
Functional | Upstream 404 is mapped to 404 City not found. (HTTP mocked) |
Unit tests use Symfony's MockHttpClient - they mock the HTTP layer, so no real API calls are made and no API key is required.
Functional tests boot the Symfony kernel in test environment. The .env.test file provides a dummy API key for service injection.
Configuration: phpunit.xml.dist
The project uses two tools to ensure code quality:
Enforces Symfony coding standards automatically.
# Check for issues (dry run)
vendor/bin/php-cs-fixer fix --dry-run --diff
# Fix all files
vendor/bin/php-cs-fixer fixConfiguration: .php-cs-fixer.dist.php
Detects type errors and bugs without running the code. Configured at level 6.
vendor/bin/phpstan analyseConfiguration: phpstan.dist.neon
- Symfony 8.0 - PHP framework
- PHP 8.4 - Readonly classes, typed properties, named arguments
- Asset Mapper - Native CSS/JS management (no Node.js)
- OpenWeatherMap API - Weather data provider
- Vanilla JS - No frontend framework
MIT - See LICENSE for details.
