Build a weather update video generator app using a free weather API and the Shotstack API
Shotstack let's you create videos using a simple API. In this tutorial, you will learn how to build a weather update video generator app using a free weather API and the Shotstack API.
Introduction
Building slideshow videos dynamically from images can be a tough job often. But such videos are more engaging than static monotonous images. Building slideshow videos dynamically is simple when you use Shotstack. Shotstack provides a video editing API that can be plugged into your existing application effortlessly.
In this article, we will build a Weather video generator using Next.js. We’ll use the WeatherStack API to get the weather details of a specific location. The flow of the application is straightforward. The user can input a place to get the weather details; Shotstack will generate a slideshow video with images of similar weather conditions.
But before you jump into the application, please make sure you have a basic understanding of React, Next.js, and Node.js.
Creating a Template in Shotstack
To create a slideshow video, it is essential to create a template in Shotstack first. The template will be used as the template for videos. To create a template in Shotstack, visit the Shotstack website and create a new account if you already don’t have one. After completing the account, you’ll be redirected to the dashboard. From the dashboard, navigate to Studio. You’ll find the link to the studio on the top navigation bar. Click on the Create Template button to create a new template.
The studio user interface is simple. You’ll need to add tracks here. Add a new track and click on the image icon to add images. Clicking on the image icon will add new pictures to the track. The photos need to be replaced with placeholder images. But before adding a placeholder image, you’ll need to add merge fields. The merge field option is available under the Settings tab in Studio. Click on the Add Merge Fields option to add merge fields. Merge Fields are key-value pairs. Define three fields here. Each for one image. You can create fields with names such as image1
, image2
, and image3
. Also, you can use a default value in the test value part. Please take a look at the image below to get a better idea.
The numbers in the image above mention the step numbers. Once the merge field is added, select the Placeholder tab and add placeholders there. Repeat this process for three images, and you’ll get your template.
Click on this icon </>
in the top right corner of the studio to get the Template ID and the API key from the sample code. Keep these two codes safe; we’ll need this in our application.
Building a Weather App in Next.js
A weather app is a very straightforward application. But to build this app, we’ll need an API key to get the weather details. For this tutorial, we’ll be using the WeatherStack API. Sign up for the website and obtain the API key. We’re not able to walk you through the process of obtaining the API key from here, but it is straightforward, and you can quickly obtain this.
For a faster development process, we’ll use a GitHub template. The template is set up with Chakra UI, Next.js, Tailwind, ES Lint, Typescript, Husky and prettier. If you don’t know what all these tools do, don’t worry. We’ll not need most of the tools here. But they come in handy when working on a more extensive application. Open the template links and click on the Use this template button to create a repo from this. Once the repo is created, clone this into your local machine to get started.
We’ll need to build two APIs. The first API will return the location data, and the second API will return the rendered video link. Though the two APIs can be easily combined into one, we’ll use two API endpoints for the separation of concern.
Creating a Weather API in Next.js
To create a new API, create a new file inside the pages/api
folder. The api
folder holds the API endpoints in Next.js. The file name act as an endpoint. For example, if you create a new file called weather.ts
here, you’ll be able to access it by visiting [localhost:3000/api/weather](http://localhost:3000/api/weather)
, assuming that your website is running on port 3000.
Create a new file called weather.ts
inside the API folder. The content of the file should look similar to this,
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { IWeatherResponse } from '../../interfaces/IWeatherResponse';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<IWeatherResponse>
) {
const { location } = req.query;
const response = await fetch(
`http://api.weatherstack.com/current?access_key=${process.env.NEXT_PUBLIC_WEATHER_API}&query=${location}`
);
const data = await response.json();
res.status(200).json(data);
}
The code is straightforward. Here, we’ll need to create an interface for the data because we are using TypeScript. The content of the interface is shown below. I think we should understand the logic here first.
We are destructuring the location
field from the request query inside the handler function. Depending on the location value, we send a GET request to the WeatherStack endpoint. The API key is stored inside a variable called NEXT_PUBLIC_WEATHER_API
in the .env
file. The response is then converted to JSON and returned to the user.
Create a folder called interfaces
in the root directory for the weather interface and create a new file called IWeatherResponse.ts
inside this folder.
export interface Request {
type: string;
query: string;
language: string;
unit: string;
}
export interface Location {
name: string;
country: string;
region: string;
lat: string;
lon: string;
timezone_id: string;
localtime: string;
localtime_epoch: number;
utc_offset: string;
}
export interface Current {
observation_time: string;
temperature: number;
weather_code: number;
weather_icons: string[];
weather_descriptions: string[];
wind_speed: number;
wind_degree: number;
wind_dir: string;
pressure: number;
precip: number;
humidity: number;
cloudcover: number;
feelslike: number;
uv_index: number;
visibility: number;
is_day: string;
}
export interface IWeatherResponse {
request: Request;
location: Location;
current: Current;
}
The interface should look like this. You can get the fields for the interface by submitting a sample request to the WeatherStack API.
Create a new folder called components
for holding the components in your root folder. Let’s create a new component inside this folder. Give it the name of Weather.tsx
. Here’s the sample code for the file:
import React from 'react';
import { Flex, Text, Input, Box, Button, Image } from '@chakra-ui/react';
import { IWeatherResponse } from '../interfaces/IWeatherResponse';
const Weather = () => {
const [location, setLocation] = React.useState<string>('');
const [weatherResult, setWeatherResult] =
React.useState<IWeatherResponse | null>(null);
const getWeather = async () => {
const response = await fetch(`/api/weather?location=${location}`);
const data = await response.json();
setWeatherResult(data);
};
return (
<Flex justifyContent={'center'} alignItems={'center'} py={'2'}>
<Box width={'full'}>
<Text
as={'h1'}
fontSize={'2xl'}
textAlign={'center'}
marginBottom={'1.2rem'}
>
Weather
</Text>
<Input
placeholder='Enter a city'
onChange={(e) => {
setLocation(e.target.value);
}}
/>
<Button marginTop={'1.2rem'} onClick={getWeather} width={'full'}>
Get Weather
</Button>
{weatherResult && (
<Box border={'2px solid green'} my={4} py={4} px={2}>
<Flex>
<Image
src={weatherResult.current.weather_icons[0]}
alt={weatherResult.current.weather_descriptions[0]}
/>
<Box marginLeft={'4'}>
<Text>
{weatherResult.location.name}, {weatherResult.location.region}
, {weatherResult.location.country}
</Text>
<Text as={'h1'} fontSize={'2xl'} fontWeight={'bold'}>
{weatherResult.current.temperature}°C
</Text>
{weatherResult.current.weather_descriptions.map((desc) => (
<Text key={desc} mr={'2'}>
{desc}
</Text>
))}
</Box>
</Flex>
</Box>
)}
</Box>
</Flex>
);
};
export default Weather;
There is little to talk about the design here. The page contains an input field and a submit button. Below the submit button, there’s a box for showing the results. The box has a green border and contains an image and details like the location, temperature, and weather description.
The component also has two states. The first state is to store the location, and the second state stores the weather result. The input field stores the field’s value in the location
state using an onChange event. If you are familiar with React, you can easily understand it.
Once the state variables are created, a new function called getWeather
is created for getting the weather result. This function sends a fetch request to the API we made earlier with the location stored in the state variable. The received data is stored in the weatherResult
state using the setter.
If the weatherResult
state is truthy, only then we show the results.
A weather icon is also sent by the WeatherStack API. It is inside the current
object available in the response body. You can understand the complete response by logging the values in the console. This getWeather
function gets invoked when someone clicks the button.
At this point, the weather app is ready. You can import this component inside your index.tsx
file in the pages
folder to see it in action.
Generating Videos Dynamically from Weather Data
If you look at the above GIF, you’ll be able to see that the weather API also returns a weather description array. These descriptions will be used for searching images and generating the slideshow.
We will be using the Pixabay API for searching images in this example. Signup for a developer account and obtain an API key. You can store this API key inside your .env
file.
Now, let’s create a new route for searching and generating videos. Create a new file called generate
inside the api
folder. We will be using Axios here for sending requests. You can install Axios by running the following command,
npm i axios
Let’s look at the generate.ts
file and discuss how it works.
import { NextApiRequest, NextApiResponse } from 'next';
import { IImageResponse } from '../../interfaces/IImageResponse';
import axios from 'axios';
interface IRenderResponse {
id: string;
success: boolean;
url: string;
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<IRenderResponse>
) {
const { query } = req.query;
try {
const response = await axios.get(
`https://pixabay.com/api/?key=${process.env.NEXT_PUBLIC_PIXABAY_API}&q=${query}&image_type=photo&orientation=horizontal&safesearch=true&per_page=3`
);
const { data } = response;
const rn = await render(data);
res.status(200).json(rn as IRenderResponse);
} catch (error) {
console.log(error);
}
}
const render = async (images: IImageResponse) => {
const mappedImages = images.hits.map((image, index) => {
return {
find: 'image' + (index + 1),
replace: image.largeImageURL,
};
});
const data = JSON.stringify({
id: process.env.NEXT_PUBLIC_SHOTSTACK_TEMPLATE_ID,
merge: mappedImages,
});
const config = {
method: 'post',
url: 'https://api.shotstack.io/v1/templates/render',
headers: {
'content-type': 'application/json',
'x-api-key': process.env.NEXT_PUBLIC_SHOTSTACK_API_KEY,
},
data: data,
};
try {
const response = await axios(config);
const { data: res } = response;
return {
id: res.response.id,
success: res.success,
url: `https://cdn.shotstack.io/au/v1/${process.env.NEXT_PUBLIC_SHOTSTACK_PUBLIC_ID}/${res.response.id}.mp4`,
};
} catch (error) {
return error;
}
};
We will look at the interface later. Let’s understand the logic first. We extract a query
parameter from the request query inside the handler function. The query parameter holds the weather description received from the weather API response.
We are sending a response to the Pixabay API inside a try-catch block. The API endpoint takes the key and query value. Other filters like orientation, safe-search, and limit are also sent. After the response is received, we destructure the data
. The data is then passed to a function called render
. The render
function generates the video from the images.
The render
function takes the images as a parameter. For each image, we create a new object with the keys of find
and replace
. The find
should be the key we set up in the Merge Fields option in the studio. Once the images are mapped, we create a JSON payload called data
using the template ID and the mappedImages
array. After the payload is created, we need to create a config object. The config object holds the configuration settings like the request method, endpoint, and header values like content type and X-API key. The X-API key is the value of the API key received from Shotstack.
Finally, we send a request with the config values using Axios. The data
object is then restructured from the response. As a response to the user, we send the rendered video’s ID, a success message, and a URL of the rendered video.
The link is generated by following a specific pattern. Shotstack assets are held in this pattern: https://cdn.shotstack.io/{{REGION}}/{{STAGE}}/{{OWNER_ID}}/{{FILENAME}}
. The region is au
by default for now. The production stage is v1
. The OWNER_ID
is the key or owner ID. You can obtain the owner ID from the dashboard. The filename is the response ID along with the file type of MP4.
The image response interface looks like the code shown below.
export interface Hit {
id: number;
pageURL: string;
type: string;
tags: string;
previewURL: string;
previewWidth: number;
previewHeight: number;
webformatURL: string;
webformatWidth: number;
webformatHeight: number;
largeImageURL: string;
fullHDURL: string;
imageURL: string;
imageWidth: number;
imageHeight: number;
imageSize: number;
views: number;
downloads: number;
likes: number;
comments: number;
user_id: number;
user: string;
userImageURL: string;
}
export interface IImageResponse {
total: number;
totalHits: number;
hits: Hit[];
}
Now, let’s update the frontend to get the video link. In the frontend, we’ll need to create a new state and use useeffect hook. The state can be defined as below,
const [renderedVideo, setRenderedVideo] =
React.useState<IRenderResponse | null>(null);
The IRenderResponse
is a simple interface.
interface IRenderResponse {
id: string;
success: boolean;
url: string;
}
The useEffect
hook should look like the below,
React.useEffect(() => {
const getVideo = async () => {
const { data } = await axios.get(
`/api/generate?query=${weatherResult?.current.weather_descriptions[0]}`
);
setRenderedVideo(data);
};
getVideo();
}, [weatherResult]);
Depending on the weatherResult
state, the getVideo
function inside the hook gets invoked. The function sends a request to the /api/generate
API endpoint with the weather description as a query. The response is then stored in the state.
Inside the return function, update the code as below,
return (
<Flex justifyContent={'center'} alignItems={'center'} py={'2'}>
<Box width={'full'}>
// .... // ....
{renderedVideo?.success && (
<Box border={'2px solid green'} my={4} py={4} px={2}>
<Text as={'h1'} fontSize={'2xl'} fontWeight={'bold'}>
{renderedVideo.success}
</Text>
<Text as={'h1'} fontSize={'2xl'} fontWeight={'bold'}>
{renderedVideo.id}
</Text>
<Link href={renderedVideo.url} isExternal>
{renderedVideo.url}
</Link>
</Box>
)}
</Box>
</Flex>
);
A new div is rendered in the DOM if the response is successful. The div shows the video’s status, ID, and URL. The application is now ready. You can test it to see if it is working correctly.
You can get the complete code from the GitHub repo.
Conclusion
Shotstack provides a straightforward API for generating videos on the fly. This article discussed how Shotstack could generate dynamic videos from weather details. We used Next.js and TypeScript for this demo, but the method is more or less similar to other frameworks.