Try Install Learn Blog API Packages GitHub
Posts

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 🙏