Handling HTTP Requests Tuesday, May 7th, 2019
In this post I will show you how to make HTTP requests to an API 🙂
The Code
This is the full source code to fetch planets from the Star Wars API and display it in a table.
record Planet {
population : String,
gravity : String,
climate : String,
name : String,
}
record Data {
results : Array(Planet),
count : Number,
}
enum Status {
Initial
Loading
Error(String)
Ok(Data)
}
store StarWars {
state status : Status = Status::Initial
fun load : Promise(Never, Void) {
sequence {
next { status = Status::Loading }
response =
"https://swapi.dev/api/planets"
|> Http.get()
|> Http.send()
object =
response.body
|> Json.parse()
|> Maybe.toResult("")
decodedResults =
decode object as Data
next { status = Status::Ok(decodedResults) }
} catch Http.ErrorResponse => error {
next { status = Status::Error("Something went wrong with the request.") }
} catch Object.Error => error {
next { status = Status::Error("The data is not what is expected.") }
} catch String => error {
next { status = Status::Error("Invalid JSON data.") }
}
}
}
routes {
* {
StarWars.load()
}
}
component Main {
connect StarWars exposing { status }
fun render : Html {
case (status) {
Status::Initial => <div></div>
Status::Loading => <div>"Loading..."</div>
Status::Error(message) => <div><{ message }></div>
Status::Ok(data) =>
<table>
<tr>
<th>"Name"</th>
<th>"Climate"</th>
<th>"Gravity"</th>
<th>"Population"</th>
</tr>
for (planet of data.results) {
<tr>
<td><{ planet.name }></td>
<td><{ planet.climate }></td>
<td><{ planet.gravity }></td>
<td><{ planet.population }></td>
</tr>
}
</table>
}
}
}
I will now explain it to you block by block.
Modelling the data
In any typed programming language, the structure of data must be defined somehow:
record Planet {
population : String,
gravity : String,
climate : String,
name : String,
}
record Data {
results : Array(Planet),
count : Number,
}
enum Status {
Initial
Loading
Error(String)
Ok(Data)
}
In Mint there are two constructs for defining data:
-
record
- which defines an object with fixed named key / value pairs -
enum
- which defines an ADT - a type which represents a fix set of possibilities
In our example
Planet
and
Data
defines the data that comes from the API and the
Status
defines the possible states of the request.
Defining the state
In Mint, global state is stored in a
store
which is globally accessible and basically works like a Component where
state is concerned. (
state
and
next
keywords
from the last article
)
store StarWars {
state status : Status = Status::Initial
fun load : Promise(Never, Void) {
...
}
}
Handling the request
The handling of an HTTP request is done in a
sequence
block, which runs each expression in it
asynchronously
in sequence in the order they are written.
What this means that it will
await all promises
Promise(error, value)
and unbox the
value
in a variable for subsequent use or raise the
error
which is handled in a
catch
block.
sequence {
next { status = Status::Loading }
response =
"https://swapi.dev/api/planets"
|> Http.get()
|> Http.send()
object =
response.body
|> Json.parse()
|> Maybe.toResult("")
decodedResults =
decode object as Data
next { status = Status::Ok(decodedResults) }
} catch Http.ErrorResponse => error {
next { status = Status::Error("Something went wrong with the request.") }
} catch Object.Error => error {
next { status = Status::Error("The data is not what is expected.") }
} catch String => error {
next { status = Status::Error("Invalid JSON data.") }
}
The
Http
module contains functions to make
Http.get(url : String)
and send
Http.send(request : Http.Request)
HTTP requests.
The next part is to parse the
JSON
content into an
Object
and then
decode
it to the type we defined earlier, then we set the
status
to
Status::Ok
or
Status::Error
according to what happened.
Routing
Mint has a built-in system for handling routes which will be featured in a different article.
In our case we define the
*
route which handles all non-handled routes, in the route we just
load the data, which in practice means when the page is loaded:
routes {
* {
StarWars.load()
}
}
Displaying the data
The last part is to display the data which we will do in the
Main
component:
component Main {
connect StarWars exposing { status }
fun render : Html {
case (status) {
Status::Initial => <div></div>
Status::Loading => <div>"Loading..."</div>
Status::Error(message) => <div><{ message }></div>
Status::Ok(data) =>
<table>
<tr>
<th>"Name"</th>
<th>"Climate"</th>
<th>"Gravity"</th>
<th>"Population"</th>
</tr>
for (planet of data.results) {
<tr>
<td><{ planet.name }></td>
<td><{ planet.climate }></td>
<td><{ planet.gravity }></td>
<td><{ planet.population }></td>
</tr>
}
</table>
}
}
}
To get the data from the store, first we need to connect the component to it
using the
connect
keyword and
expose
the
status
state so it can be used in the scope of the component.
Connecting a component to a store makes it so that the component re-renders when the data in the store changes.
Then based on the
status
we render different content:
-
Status::Initial
- we display nothing -
Status::Loading
- we display a loading message -
Status::Error(message)
- we display the error message -
Status::Ok(data)
- we display the data
And there you have it, thank you for reading 🙏