I see some people complaining about boilerplate here, so I thought I’d share my HTTP Client for general feedback (or as an example for anyone that’s interested). It has:
- ability to do a get request (still need to add post etc)
- ability to unauthenticate a user or refresh auth token
- comes with a provider component and a hook
// HTTPClient.res
open Http
type successCB = (Js.Json.t, status)=> unit
type errorCB = unit => unit
type cleanup = unit => unit
type t = {
get: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB) => cleanup,
// post: (~relativeURL: string, ~successCB: successCB, ~errorCB: errorCB, ~body: Js.Json.t) => unit,
}
let newAuthenicatedClient = (~baseURL, ~unauthenticate, ~getAuthToken) => {
let getTokenCB = cb => {
open Promise
getAuthToken()
->Promise.then(token => {
cb(token)
resolve()
})
->catch(_ => {
unauthenticate()
resolve()
})
->ignore
}
let get = (~relativeURL, ~successCB, ~errorCB) => {
let request = makeXMLHttpRequest()
request->addEventListener("load", () => {
let status = request->statusCode->codeToStatus
let response = request->response->toJson
switch status {
| StatusUnauthorized => unauthenticate()
| _ => successCB(response, status)
}
})
request->Http.addEventListener("error", () => {
errorCB()
})
request->Http.open_("GET", baseURL ++ relativeURL)
getTokenCB(token => {
request->Http.setHeader("Authorization", "Bearer " ++ token)
request->Http.send
})
() => request->abort
}
{
get: get,
}
}
let context = React.createContext(
newAuthenicatedClient(
~baseURL="",
~unauthenticate=() => (),
~getAuthToken=() => Promise.resolve(""),
),
)
module Provider = {
let provider = React.Context.provider(context)
@react.component
let make = (~client: t, ~children) => {
React.createElement(provider, {"value": client, "children": children})
}
}
let use = () => React.useContext(context)
It’s a lot, but the usage is quite nice (and most people can probably cut out a lot of the auth logic).
Usage:
Wrap the client somewhere at the top of your component tree (I’m using Auth0 btw, just if anyone is wondering where my token logic is coming from):
let client = HttpClient.newAuthenicatedClient(
~baseURL="http://localhost:4000",
~unauthenticate=() => logout({returnTo: Window.Location.origin}),
~getAuthToken=() => {
open Promise
getIdTokenClaims()->Promise.then(tokenClaims => resolve(tokenClaims.__raw))
},
)
<HttpClient.Provider client> children </HttpClient.Provider>
Then you can easily access the client anywhere in your component tree with the Hook:
let client = HttpClient.use()
Then you can inject it into some data layer, or run it in useEffect to cleanup on component unmount. You can do a GET request as follows:
React.useEffect0(() => {
let cleanup = client.get(
~relativeURL="/some/relative/path",
~successCB=(json, status) => {
Js.log(json)
},
~errorCB=() => Js.log("error"),
)
Some(() => cleanup())
}
I’d love some feedback, specifically around the Promise logic as that’s also new to me. Hope this is useful 
Lastly, I do have some added utils in my Http bindings, I’ll just dump the whole module here
// Http.res
type request
type response
type status =
| StatusOk //200
| StatusCreated //201
| StatusNoContent //204
| StatusBadRequest //400
| StatusUnauthorized //401 unauthenticated
| StatusForbidden //403 unauthorized
| StatusNotFound //404
| StatusInternalServerError //500
let codeToStatus = statusInt =>
switch statusInt {
| 200 => StatusOk
| 201 => StatusCreated
| 204 => StatusNoContent
| 400 => StatusBadRequest
| 401 => StatusUnauthorized
| 403 => StatusForbidden
| 404 => StatusNotFound
| _ => StatusInternalServerError
}
@new external makeXMLHttpRequest: unit => request = "XMLHttpRequest"
@send external addEventListener: (request, string, unit => unit) => unit = "addEventListener"
@get external response: request => response = "response"
@get external statusCode: request => int = "status"
@send external open_: (request, string, string) => unit = "open"
@send external setHeader: (request, string, string) => unit = "setRequestHeader"
@send external send: request => unit = "send"
@send external abort: request => unit = "abort"
@scope("JSON") @val
external toJson: response => Js.Json.t = "parse"