Photo by Rob Fuller on Unsplash
Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 7
Fetching data from our server
The next step in our journey is to hook up our frontend to the backend.
In the last article we made things look a bit better and we set up some logic so that a loading spinner appears when we click on the submit
button.
Now we need to use the handleSubmit
method to actually fetch data from our server.
Spin up the server
In case we don't recall from Part 1 of this series, our server is set up in a Python environment using FastAPI
.
If our directory structure is like this
.
├── fast-api-vue-stock
│ ├── client
│ └── server
│ └── main.py
we can open a terminal/command prompt and type the following
cd /path/to/server
env\Scripts\activate
uvicorn main:app --reload
This assumes that we already have the same setup as was described in Part 1 of the tutorial. By typing this then our server should be running at port 8000.
Connecting to the server
Create a querystring
For those who may not recall, the format of the url that we will be consuming in the backend is of the format /quote/{ticker}/{interval}
.
Then, any date information, as it is optional, is passed in via query parameters in the format YYYY-mm-dd
.
So, within the handleSubmit
method in Selections.vue
we first need to create a query object and add the start and end dates if they exist.
const query = {}
if (!!state.startDate) query.start = state.startDate;
if (!!state.endDate) query.end = state.endDate;
Then, we can just map through the keys of the query and use the encodeURIComponent built-in method to convert our dates into the correct format. It should be noted that since we're only doing this with two values the method that will be presented below may be a little overkill, but the idea is that this method can be used for any number of query parameters so it's worth it to present.
const queryString = Object.keys(query)
.filter((key) => !!query[key])
.map((key) => {
return (
`${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`
);
})
.join('&')
To see what this method does, let's use the following object as an example
query = {
"hello": "world",
"hashnode": [1,2,3],
"vue": {
"is": "cool"
},
"fastapi": "is as well"
}
If we then look at the resulting string, we get
'hello=world&hashnode=1%2C2%2C3&vue=%5Bobject%20Object%5D&fastapi=is%20as%20well'
and we notice that there is an object%20Object
where the value for the vue
key should be.
This shows us that we can't just use this method for any deeply nested structures, but it instead works for simple key-value stores.
If we instead set
query = {
"hello": "world",
"hashnode": [1,2,3],
"vue": ["is", "cool"],
"fastapi": "is as well"
}
then the resulting string would look like 'hello=world&hashnode=1%2C2%2C3&vue=is%2Ccool&fastapi=is%20as%20well'
which would work much better.
There is an argument that you shouldn't need to do something like this when you're passing in the data yourself, but I've found that it can't hurt to just be extra careful with these sorts of things.
Now we should have a valid query string for our start
and end
dates, if they exist.
Convert human-readable interval into yfinance style
The next step is to convert the interval into the style that yfinance
needs.
For human readability, we're presenting our intervals as "Daily", "Weekly", and "Monthly", but these won't work in yfinance
and we should actually get errors if we try to pass these values in because we've set the allowable input values to be one of 1d
, 1wk
, or 1mo
.
So, this is as simple as just using a nested JavaScript ternary operator.
const interval =
state.interval === 'Daily'
? '1d'
: state.interval === 'Weekly'
? '1wk'
: '1mo';
This sets the interval
constant to 1d
if the state's interval value is Daily
, otherwise if the state's interval value is Weekly
it sets it to 1wk
, otherwise it sets it to 1mo
.
The equivalent code in if
statements would be
let interval;
if (state.interval === 'Daily') {
interval = '1d'
} else if (state.interval === 'Weekly') {
interval = '1wk'
} else {
interval = '1mo'
}
Create the URL string and add the query if it exists
We can now create the URL that will be used to fetch the data from our server.
We first set up the basic url with the symbol
and interval
values
let url = `http://localhost:8000/quote/${state.symbol}/${interval}`;
and we then need to append any query string, if it exists.
if (queryString.length) url = `${url}?${queryString}`;
We do this because we need to put a ?
at the beginning of the query string.
The next step is to fetch the data from the server using the built-in fetch
API.
const response = await fetch(url);
const res = await response.json();
Parse the dates and format data
Parse dates
The next step is to update the state
's startDate
and endDate
with the values obtained from the server.
This is done because one or both of the dates input by the user may have been weekends or holidays, or any other myriad possibilities.
We assume that the data we've received from the server contains valid dates, so the simplest way to do this is to just set state.startDate
to the earliest date, and state.endDate
to the latest date.
First, we want to create an anonymous function that will format the dates into the format we're currently using on the site.
const formatDate = (d) => new Date(d).toISOString().substr(0, 10);
and then we sort the dates, and set each one
const dates = res.data.map((e) => e.date).sort();
state.startDate = formatDate(dates[0])
state.endDate = formatDate(dates[dates.length - 1])
Format the data into our desired output
This last step is going to come slightly out of left field; we are going to format the data in a format that will eventually be used by our charting platform.
We're going to have the x
coordinates be the dates and the y
coordinates be the data, whether volume or stock prices.
const data = res.data.map((e) => ({
x: new Date(e.date),
y: e.data
}));
const volData = res.data.map((e) => ({
x: new Date(e.date),
y: e.volume ?? 0
}));
The entire code of the handleSubmit
method should now look like this
const handleSubmit = async () => {
v$.value.$validate();
if (!v$.value.$invalid) {
loading.value = true;
// Create the querystring
const query = {}
if (!!state.startDate) query.start = state.startDate;
if (!!state.endDate) query.end = state.endDate;
const queryString = Object.keys(query)
.filter((key) => !!query[key])
.map((key) => {
return (
`${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`
);
})
.join('&')
// Convert human-readable interval into yfinance style
const interval =
state.interval === 'Daily'
? '1d'
: state.interval === 'Weekly'
? '1wk'
: '1mo';
// Create URL string and add query if it exists
let url = `http://localhost:8000/quote/${state.symbol}/${interval}`;
if (queryString.length) url = `${url}?${queryString}`;
const response = await fetch(url);
const res = await response.json();
// Parse dates
const formatDate = (d) => new Date(d).toISOString().substr(0, 10);
const dates = res.data.map((e) => e.date).sort();
state.startDate = formatDate(dates[0])
state.endDate = formatDate(dates[dates.length - 1])
// Format the data
const data = res.data.map((e) => ({
x: new Date(e.date),
y: e.data
}));
const volData = res.data.map((e) => ({
x: new Date(e.date),
y: e.volume ?? 0
}));
loading.value = false;
}
};
You'll notice that we removed the setTimeout and added an entry at the bottom to set the loading
spinner to false.
Also, we needed to make the function itself async
in order to utilize the await
keyword.
In the next article we'll discuss error handling, because having this type of functionality not wrapped in try
and catch
blocks is a recipe for disaster.
We'll also add in a dialog to alert the user when an error has taken place.