This exercise allows you to practice and apply the concepts and techniques taught in class.
Upon completion of this exercise, you will be able to:
- Send HTTP requests to an external API using Axios and TypeScript
- Create type aliases to describe responses from an API
- Use type annotations for Promises
- Use type assertions to interact with DOM elements
In this lab, you will create a weather app that allows the user to type the name of a location and get the current weather for that location.
For this application, we will use some APIs from Open-Meteo and the axios library for HTTP requests. In particular, we will interact with these two APIs from Open-Meteo:
Geocoding API (docs):
- This API will allow us to search for a specific location by name.
- For example, we can get a list of 3 locations that match the name "Berlin" by sending a GET request to this URL: https://geocoding-api.open-meteo.com/v1/search?name=Berlin&count=3.
- It will return a list of locations matching that name and some information for each location (country, latitude, longitude, etc.).
Weather Forecast API (docs):
- This API will allow us to get the current weather for a specific location, using the location's coordinates (latitude and longitude).
- For example, if we want to get the current weather in Berlin, Germany (latitude 52.52437; longitude 13.41053), we would send a GET request to this URL: https://api.open-meteo.com/v1/forecast?latitude=52.52437&longitude=13.41053¤t_weather=true&models=icon_global
- It will return info about the current weather for that specific location (temperature, wind speed, wind direction, etc.).
To begin, follow these steps:
- Fork this repo
- Clone this repo
- Open the project folder in VSCode
Then, run the following commands in your terminal 👇👇
npm install
(this will install all the dependencies)npm run dev
Finally, open the URL http://localhost:5173 in your browser. You should see a page with a form (the functionality for the form is not working yet, that's what we will implement during this lab 😉).
-
Upon completion, run the following commands:
git add . git commit -m "Completed lab" git push origin main
-
Create a Pull Request and submit your assignment.
The initial code provided for this lab is a Vite application with TypeScript already configured.
To help you get started quickly, we have created the initial structure and added some initial code. For example, you'll find these HTML and CSS files:
index.html
: this is the HTML file that will be rendered in the browser. If you open it, you will see that we've added the HTML code for the form and some containers that we will use to display the info about the location and weather.src/style.css
: we have also included some CSS, so that you can focus on the functionality.
Our TypeScript code will be organized in 3 different files:
src/types.ts
: this is the file where we'll include all TypeScript type definitions.src/utils.ts
: in this file we will create several utility functions (reusable functions that perform a specific task and can be used in other parts of our application).src/main.ts
: in this file, we will add the main logic of our application.
If you open src/types.ts
, you will see that we have already added 2 type definitions:
Location
: a type alias that defines the structure of an object with information about a given location.LocationResponse
: a type alias that defines the structure of the response that we get from the API when we send a request to get the details of a specific location.
If you open src/utils.ts
, you will find this initial code:
- We are importing
axios
(which is already installed as a dependency) - We also import some type definitions from
src/types.ts
. - And, we have already created a function
getLocation()
. This function takes the name of a location as an argument and will send a request to the API to get the details of that location. It returns a promise that resolves to the typeLocationResponse
In src/types.ts
, create a new type alias with the name WeatherResponse
. This type alias should describe the structure that we get from the API when we send a request to get the weather for a specific location.
To get an example of a response, you can send a GET request to this URL: https://api.open-meteo.com/v1/forecast?latitude=52.52437&longitude=13.41053¤t_weather=true&models=icon_global. Then, create a type alias that follows that pattern and export it so that it can be used in other files.
Tip 1
To see the response in a human-friendly format, you can use Postman:
Once you can see the response in human-friendly format, it will be much easier to create the type alias.
Tip 2
Another option, when you have to create a type alias for complex data is to use ChatGPT or a similar AI tool to generate the type alias for you:
Note: there's some cases in which ChatGPT will not be able to give you the right answer. For example, if there's optional properties and they're not included in the example provided, or when some properties can have different types (i.e., union types).
Solution
// src/types.ts
// ...
export type WeatherResponse = {
latitude: number;
longitude: number;
generationtime_ms: number;
utc_offset_seconds: number;
timezone: string;
timezone_abbreviation: string;
elevation: number;
current_weather_units: {
time: string;
interval: string;
temperature: string;
windspeed: string;
winddirection: string;
is_day: string;
weathercode: string;
};
current_weather: {
time: string;
interval: number;
temperature: number;
windspeed: number;
winddirection: number;
is_day: number;
weathercode: number;
};
};
Note: don't forget to export it, so that you can use this type definition in other files.
In this step, you will create the signature for the function getCurrentWeather()
. This function will send a request to the Weather Forecast API and return the API response.
For now, we will just define the signature of our function (ie. we will declare the function, specifying which parameters it takes and what it returns but we will not implement the logic of the function).
Open the file src/utils.ts
and declare a function with this signature:
- Name of the function:
getCurrentWeather()
- Parameters:
locationDetails
(an object of typeLocation
)
- Return value:
- Our function should return a
Promise
that resolves to the typeWeatherResponse
- Example:
Promise<WeatherResponse>
- Our function should return a
- Note: for now, don't worry about the code inside the function (we will do that in the next step)
Solution
// src/utils.ts
import { LocationResponse, Location, WeatherResponse } from "./types.ts";
// ...
export function getCurrentWeather(locationDetails: Location): Promise<WeatherResponse> {
// ...
}
Next, you will implement the logic of the function getCurrentWeather()
. Your function needs to do the following:
- Send the API Request:
- Use axios to send a GET request to this URL:
https://api.open-meteo.com/v1/forecast?latitude=${locationDetails.latitude}&longitude=${locationDetails.longitude}¤t_weather=true&models=icon_global
(wherelocationDetails.latitude
andlocationDetails.longitude
are the coordinates that we get from the argumentlocationDetails
).
- Handle the Response:
- Return a promise that resolves with the data from the API response.
- Note: you can use the
.then()
method to extract the data property from the response object.
Solution
// src/utils.ts
import { LocationResponse, Location, WeatherResponse } from "./types.ts";
// ...
export function getCurrentWeather(locationDetails: Location): Promise<WeatherResponse> {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${locationDetails.latitude}&longitude=${locationDetails.longitude}¤t_weather=true&models=icon_global`;
return axios.get(url).then((response) => response.data);
}
In this step, we will define a function that will receive the details of a location and display them in the user interface.
In src/utils.ts
, create a function with this signature:
- Name of the function:
displayLocation()
- Parameters:
locationDetails
(an object of typeLocation
)
- Return value:
- For this function, we're not interested in the return value.
Also, make sure to export your function so that it can be used in other files.
Solution
// src/utils.ts
// ...
export function displayLocation(locationDetails: Location) {
// ...
}
Note: you can also explicitly state that we're not interested in the return value using void
:
// src/utils.ts
// ...
export function displayLocation(locationDetails: Location): void {
// ...
}
Next, implement the logic for the function displayLocation()
. This function should do some DOM manipulation to display in the user interface the details of a location that we receive as an argument. In particular, it should do the following:
- In the HTML element with the id
location-name
, you should display the name of the location. - In the HTML element with the id
country
, you should display the country.
Hint
You can get a reference to an HTML elements using the method getElementById()
and a type assertion. Once you have a reference, you can modify the content using innerText
.
Example:
const myCoolHtmlElm = document.getElementById('my-cool-id') as HTMLElement;
myCoolHtmlElm.innerText = "Hello World";
Solution
// src/utils.ts
// ...
export function displayLocation(locationDetails: Location) {
// display location name
const locationNameElm = document.getElementById('location-name') as HTMLElement;
locationNameElm.innerText = "" + locationDetails.name;
// display country
const countryElm = document.getElementById('country') as HTMLElement;
countryElm.innerText = "(" + locationDetails.country + ")";
}
In this step, we will define a function that will receive the data about about the weather and display that info in the user interface.
In src/utils.ts
, create a function with this signature:
- Name of the function:
displayWeatherData()
- Parameters:
obj
(an object of typeWeatherResponse
)
- Return value:
- For this function, we're not interested in the return value.
Solution
// src/utils.ts
// ...
export function displayWeatherData(obj: WeatherResponse) {
// ...
}
Note: you can also explicitly state that we're not interested in the return value using void
:
// src/utils.ts
// ...
export function displayWeatherData(obj: WeatherResponse): void {
// ...
}
Next, implement the logic for the function displayWeatherData()
. This function should do some DOM manipulation to display in the user interface the info about the weather. In particular, it should do the following:
- In the HTML element with the id
temperature
, you should display the temperature, including the units (e.g.20.6 °C
). - In the HTML element with the id
windspeed
, you should display the wind speed (e.g.3.4 km/h
). - In the HTML element with the id
winddirection
, you should display the wind direction (e.g.32 °
).
Solution
// src/utils.ts
// ...
export function displayWeatherData(obj: WeatherResponse) {
// display temperature
const temperatureElm = document.getElementById('temperature') as HTMLElement;
const temperature = obj.current_weather.temperature;
const temperatureUnits = obj.current_weather_units.temperature;
temperatureElm.innerText = `Temperature: ${temperature} ${temperatureUnits}`;
// display wind speed
const windspeedElm = document.getElementById('windspeed') as HTMLElement;
const windspeed = obj.current_weather.windspeed;
const windspeedUnits = obj.current_weather_units.windspeed;
windspeedElm.innerText = `Wind Speed: ${windspeed} ${windspeedUnits}`;
// display wind direction
const winddirectionElm = document.getElementById('winddirection') as HTMLElement;
const winddirection = obj.current_weather.winddirection;
const winddirectionUnits = obj.current_weather_units.winddirection;
winddirectionElm.innerText = `Wind Direction: ${winddirection} ${winddirectionUnits}`;
}
Now that we have all our type definitions and utility functions ready, we will implement the functionality so that, when the user types the name of a location, we can display the weather for that location.
In this step, we will add an event listener to detect when the user submits the form.
In src/main.ts
, create the code to do the following:
- Add an event listener to detect if the user submits the form (note: the form has the id
weather-form
). - Inside the code for that event listener, invoke the method
event.preventDefault()
, so that the page does not reload when the user submits the form. - Also inside the event listener, do a
console.log()
with the message "The user has submitted the form".
Once you have completed these steps, when the user submits the form, you should see a message "The user has submitted the form" in the console.
Solution
// src/main.ts
const form = document.getElementById("weather-form") as HTMLFormElement;
form.addEventListener('submit', (event) => {
event.preventDefault(); // Prevent the default form submission behavior
console.log("The user has submitted the form");
});
Next, modify the event listener you created in the previous step so that, when the user submits, you display in the console the name of the location that they have entered (example: "The user has submitted the form and is searching for a location with this name... [Berlin]").
Solution
// src/main.ts
const form = document.getElementById("weather-form") as HTMLFormElement;
form.addEventListener('submit', (event) => {
event.preventDefault(); // Prevent the default form submission behavior
const locationInput = document.getElementById("location") as HTMLInputElement;
const locationName = locationInput.value;
console.log(`The user has submitted the form and is searching for a location with this name... ${locationName} `);
locationInput.value = ""; // Clear the form
});
Now, you will need to put all the pieces together! Modify the code in src/main.ts
so that, when the user submits the form, we display info about the location and the current weather.
Hints
You will need to do the following:
- When the user submits, invoke the function
getLocation()
to get the details of the desired location from the API. This function returns a promise, so you can handle the response with.then().catch()
. - Once you have the response from the API (i.e., inside the
.then()
), use the first result (the API returns an array, so you can get the element with index zero) and invoke the functionsdisplayLocation()
(to display those details in the UI) andgetCurrentWeather()
(to get the current weather for that location). - Once you have the info about the current weather, invoke the function
displayWeatherData()
These steps are more complex and we're putting many pieces together. Remember that you can add a console.log()
to see what data you have (for example, you can add a console.log()
to see what data you get from the API) 😉
Solution
// src/main.ts
import { getLocation, getCurrentWeather, displayLocation, displayWeatherData } from './utils.ts';
const form = document.getElementById("weather-form") as HTMLFormElement;
form.addEventListener('submit', (event) => {
event.preventDefault(); // Prevent the default form submission behavior
const locationInput = document.getElementById("location") as HTMLInputElement;
const locationName = locationInput.value;
locationInput.value = ""; // Clear the form
getLocation(locationName)
.then((response) => {
if(response.results){
// Get the first result (the api may provide multiple results if there's multiple locations with the same or similar names, we will just use the first one for simplicity)
const location = response.results[0];
// Display info about the location
displayLocation(location);
// Get info about the weather for that location
return getCurrentWeather(location);
} else {
// If there's no results, throw an error
throw new Error('Location not found');
}
})
.then((weatherData) => {
// Display info about the weather
displayWeatherData(weatherData);
})
.catch((error) => {
console.log("Error getting weather data");
console.log(error);
});
});
Congratulations, if you've reached this point, the main functionality will be working! In this iteration, we will improve the user experience by adding a background image that reflects the weather in each location (for example, if you search for the weather in Berlin and it's cloudy, we will display a background image with clouds).
So that you can focus on the functionality, we have already included all the images you will need (in the directory /public/images/background
) and some CSS rules (in src/style.css
).
In this step, we will define a function that will receive some details about the weather and, based on those details, it will do some DOM manipulation so that those CSS rules are applied.
In src/utils.ts
, create a function with this signature:
- Name of the function:
updateBackground()
- Parameters:
weatherCode
(a number)isDay
(also a number)
- Return value:
- For this function, we're not interested in the return value.
Solution
// src/utils.ts
// ...
export function updateBackground(weatherCode: number, isDay: number) {
// ...
}
Note: you can also explicitly state that we're not interested in the return value using void
:
// src/utils.ts
// ...
export function updateBackground(weatherCode: number, isDay: number): void {
// ...
}
At the end of the file src/style.css
, we have included some CSS rules that modify the background based on the class of the <body>
tag (for example, if the <body>
tag has the class sunny
, a sunny background will be applied).
Implement the logic for the function updateBackground()
so that it changes the class of the body
tag based on weatherCode
and isDay
, following the table below:
First character of weatherCode |
isDay |
Class name |
---|---|---|
0 or 1 | 0 | sunny-night |
1 | sunny |
|
2 | 0 | partly-cloudy-night |
1 | partly-cloudy |
|
3 | any value | cloudy |
4 | any value | foggy |
5 | any value | drizzle |
6 | any value | rain |
7 | any value | snow |
8 | any value | showers |
9 | any value | thunderstorm |
For example:
- If the first character of
weatherCode
is0
andisDay
is also0
, your function should change the class of the HTMLbody
tag tosunny-night
. - If the first character of
weatherCode
is3
, your function should change the class of the HTMLbody
tag tocloudy
.
Hint
You can change the class using the property className
. For example:
document.body.className = "sunny-night";
Solution
// src/utils.ts
// ...
export function updateBackground(weatherCode: number, isDay: number) {
const firstCharacter = weatherCode.toString().charAt(0);
switch(firstCharacter){
case "0":
case "1":
if(isDay === 0){
document.body.className = "sunny-night";
} else {
document.body.className = "sunny";
}
break;
case "2":
if(isDay === 0){
document.body.className = "partly-cloudy-night";
} else {
document.body.className = "partly-cloudy";
}
break;
case "3":
document.body.className = "cloudy";
break;
case "4":
document.body.className = "foggy";
break;
case "5":
document.body.className = "drizzle";
break;
case "6":
document.body.className = "rain";
break;
case "7":
document.body.className = "snow";
break;
case "8":
document.body.className = "showers";
break;
case "9":
document.body.className = "thunderstorm";
break;
default:
document.body.className = "";
break;
}
}
Finally, you will need to update the file src/main.ts
and invoke the function updateBackground()
once you have the details about the weather.
Hint
You can invoke updateBackground()
, right after invoking displayWeatherData()
(in src/main.ts
).
When you invoke updateBackground()
, make sure to pass the expected arguments.
Solution
First, make sure to import the function updateBackground()
in src/main.ts
:
// src/main.ts
import { getLocation, getCurrentWeather, displayLocation, displayWeatherData, updateBackground } from './utils';
Then, modify the code to invoke it once we have the details about the weather:
// src/main.ts
// ...
form.addEventListener('submit', (event) => {
// ...
getLocation(locationName)
.then((response) => {
// ...
})
.then((weatherData) => {
// Display info about the weather
displayWeatherData(weatherData);
// Update background
updateBackground(weatherData.current_weather.weathercode, weatherData.current_weather.is_day); // <== ADD THIS
})
.catch((error) => {
// ...
});
});
Happy coding! ❤️
This project uses weather data from Open-Meteo, licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0), and images from Pixabay.
I am stuck and don't know how to solve the problem or where to start. What should I do?
If you are stuck in your code and don't know how to solve the problem or where to start, you should take a step back and try to form a clear question about the specific issue you are facing. This will help you narrow down the problem and come up with potential solutions.
For example, is it a concept that you don't understand, or are you receiving an error message that you don't know how to fix? It is usually helpful to try to state the problem as clearly as possible, including any error messages you are receiving. This can help you communicate the issue to others and potentially get help from classmates or online resources.
Once you have a clear understanding of the problem, you will be able to start working toward the solution.
When I run the app, I get an error "vite: not found"
Make sure to install all the dependencies running this command in your terminal:
npm install
How can I run the app on a different port?
By default, Vite will run on the port 5173
. If you need to run the app on a different port, you can use this command:
npm run dev -- --port=3001
Do I need to add type annotations to everything?
No. In many cases, TypeScript can infer the types from the context and using implicit types can make your code more clear and readable.
For example:
function calcTotal(numberOfProducts: number, price: number): number {
const total = numberOfProducts * price; // implicit (TypeScript can infer that "total" will be a number)
return total;
}
I get an error "Cannot find name 'abc'"
If you get an error "Cannot find name 'abc'" (for example, Cannot find name 'WeatherResponse'
), it can be because your code is not exported/imported correctly.
Make sure to export your type definitions and functions, so that they can be used in other files. For example:
// src/types.ts
// ...
export type WeatherResponse = {
// ...
}
Also, when you use a type definition or a function from another file, make sure to import it. For example:
// src/utils.ts
// ...
import { LocationResponse, Location, WeatherResponse } from "./types.ts";
I am unable to push changes to the repository. What should I do?
There are a couple of possible reasons why you may be unable to push changes to a Git repository:
-
You have not committed your changes: Before you can push your changes to the repository, you need to commit them using the
git commit
command. Make sure you have committed your changes and try pushing again. To do this, run the following terminal commands from the project folder:git add . git commit -m "Your commit message" git push
-
You do not have permission to push to the repository: If you have cloned the repository directly from the main Ironhack repository without making a Fork first, you do not have write access to the repository. To check which remote repository you have cloned, run the following terminal command from the project folder:
git remote -v
If the link shown is the same as the main Ironhack repository, you will need to fork the repository to your GitHub account first, and then clone your fork to your local machine to be able to push the changes.
Note: You may want to make a copy of the code you have locally, to avoid losing it in the process.