Skip to content

API Surface

The package exposes browser APIs under the WebAPI namespace. In normal application code, add the package to rescript.json:

{
"dependencies": ["@rescript/webapi"]
}

Use WebAPI.DOM.window for the browser window. Interface-specific methods live on public modules such as WebAPI.Window, WebAPI.Location, WebAPI.Document, WebAPI.Element, WebAPI.Request, and WebAPI.Response.

let location = WebAPI.DOM.window->WebAPI.Window.location
let href = location.href
location->WebAPI.Location.reload

The public API is flat under WebAPI. Use owner modules directly rather than generated helper modules.

let req: WebAPI.Request.t = WebAPI.Request.fromURL("https://example.com")
let headers = WebAPI.Headers.make()
let document = WebAPI.DOM.window->WebAPI.Window.document
let element = document->WebAPI.Document.createElement("button")

Generated implementation modules such as DomTypes, FetchTypes, EventTypes, and UiEventsTypes are internal. If you need to name a public type, use the public interface module’s t type when it has one, or use a dedicated public type module. Otherwise, let the value type be inferred from constructors and accessors.

Feature names select related bindings and their transitive dependencies. They do not add another namespace segment. For example, selecting WebAPI.Fetch enables flat modules such as WebAPI.Fetch, WebAPI.Request, WebAPI.Response, and WebAPI.Headers.

{
"dependencies": [
{
"name": "@rescript/webapi",
"features": ["WebAPI.Fetch", "WebAPI.HTML"]
}
]
}

The supported feature bundles are:

WebAPI.DOM WebAPI.Event WebAPI.DOMPlatform
WebAPI.DOMNodes WebAPI.File WebAPI.HTML
WebAPI.Window WebAPI.CSSOM WebAPI.CSSFontLoading
WebAPI.Geometry WebAPI.SVG WebAPI.Animation
WebAPI.Device WebAPI.Navigator WebAPI.Canvas
WebAPI.URL WebAPI.Fetch WebAPI.UIEvents
WebAPI.Observers WebAPI.Media WebAPI.WebAudio
WebAPI.Storage WebAPI.Messaging WebAPI.Workers
WebAPI.Crypto WebAPI.Performance WebAPI.ViewTransitions
WebAPI.SafeFetch

The package owns each bundle as one internal source folder. Those internal folder feature names are an implementation detail; consumers should use only the qualified names above. WebAPI.SafeFetch transitively enables WebAPI.Fetch, so consumers that want the result-based helpers can select only the safe feature:

{
"experimental-features": {
"LetUnwrap": true
},
"dependencies": [
{
"name": "@rescript/webapi",
"features": ["WebAPI.SafeFetch"]
}
]
}

The let? examples below require the experimental LetUnwrap setting with the current ReScript 13 alpha compiler. The SafeFetch API itself can also be used with ordinary switch expressions.

Use WebAPI.Fetch.fetch for string URLs and WebAPI.Fetch.fetchWithRequest when you already have a WebAPI.Request.t.

let response = await WebAPI.Fetch.fetch("https://example.com/api/users")
if response.ok {
let body = await response->WebAPI.Response.json
Console.log(body)
} else {
Console.log("Request failed")
Console.log(response.status)
}

For JSON-style POST requests, construct headers with HeadersInit and the body with BodyInit.

let response = await WebAPI.Fetch.fetch(
"https://example.com/api/users",
~init={
method: "POST",
headers: WebAPI.HeadersInit.fromDict(
dict{
"Accept": "application/json",
"Content-Type": "application/json",
},
),
body: WebAPI.BodyInit.fromString(`{"name":"ReScript"}`),
},
)
let text = await response->WebAPI.Response.text

WebAPI.Fetch and WebAPI.Response are the raw browser bindings and remain unchanged. Use the separate, opt-in WebAPI.SafeFetch module when request, HTTP status, and body-reading failures should be handled as result values. The separate feature is an intentional breaking change for consumers that restrict enabled features: add WebAPI.SafeFetch to use these helpers. Existing raw Fetch clients are unaffected.

Stage Result Preserved context
Fetch throws or rejects Error(#FetchRejected(cause)) Original rejection as exn
Response is not OK Error(#ResponseNotOk({response})) Complete response
Body reader rejects Error(#BodyReadRejected({response, cause})) Response and original rejection

SafeFetch.fetch accepts a URL, while SafeFetch.fetchWithRequest accepts an existing WebAPI.Request.t. Both promises fulfill with a result for these expected failures. A fulfilled response is classified with the platform’s response.ok value, so a 404 or 500 is distinct from a request that did not produce a response. The public fetchError type contains #FetchRejected(exn) and #ResponseNotOk({response}); bodyError contains #BodyReadRejected(readError). readError has response and cause fields, and response<'body> retains the original response alongside the successful body. The function signatures expose extensible error rows ([> fetchError] and [> bodyError]), so application errors can be added directly with let?, without mapError.

let fetchJson = async url => {
let? Ok(response) = await WebAPI.SafeFetch.fetch(url)
let? Ok({body}) = await response->WebAPI.SafeFetch.json
Ok(body)
}

The body reader reports malformed JSON as #BodyReadRejected; valid JSON with the wrong shape is an application decoding failure instead. Here is one way to compose the stages and retain an application error variant in the result:

type user = {name: string}
let decodeUser = json =>
switch json {
| JSON.Object(dict{"name": JSON.String(name)}) => Ok({name: name})
| _ => Error(#InvalidUser)
}
let loadUser = async url => {
let? Ok(response) = await WebAPI.SafeFetch.fetch(url)
let? Ok({body}) = await response->WebAPI.SafeFetch.json
let? Ok(user) = body->decodeUser
Ok(user)
}

The composed result has open error rows, so its caller can handle each stage exhaustively, including the application-specific #InvalidUser:

switch await loadUser("https://example.com/api/users/42") {
| Error(#FetchRejected(cause)) => reportUnavailable(cause)
| Error(#ResponseNotOk({response})) => handleHttpError(response.status)
| Error(#BodyReadRejected({response, cause})) => handleInvalidBody(response.status, cause)
| Error(#InvalidUser) => handleInvalidUser()
| Ok(user) => showUser(user)
}

Use checkOk to classify a response obtained from the raw API without reading its body. Its non-success result is tagged as #ResponseNotOk({response}).

let response = await WebAPI.Fetch.fetch("https://example.com/api/users")
switch response->WebAPI.SafeFetch.checkOk {
| Ok(response) => useResponse(response)
| Error(#ResponseNotOk({response})) => handleHttpError(response.status)
}

A non-OK response remains readable. This is useful when a server returns structured error details; reading that body can itself fail with #BodyReadRejected.

switch await WebAPI.SafeFetch.fetch("https://example.com/api/users/42") {
| Error(#ResponseNotOk({response})) =>
switch await response->WebAPI.SafeFetch.text {
| Ok({body}) => Console.error(body)
| Error(#BodyReadRejected(_)) =>
Console.error(`Request failed with ${Int.toString(response.status)}`)
}
| Error(#FetchRejected(cause)) => reportUnavailable(cause)
| Ok(response) => useResponse(response)
}

The safe body readers cover arrayBuffer, blob, bytes, formData, json, and text. A successful read contains both the body and original response, preserving status, headers, URL, and other metadata. Reading consumes the native body; clone the response before reading when another consumer also needs it.

SafeFetch.json returns JSON.t; validating that JSON against an application’s domain types remains the application’s responsibility. A body read preserves its response and original rejection in the readError payload. This keeps malformed JSON text separate from valid JSON that does not match an application schema.

BodyInit has constructors for the common JavaScript body shapes.

let params = WebAPI.URLSearchParams.fromString("q=rescript")
let formData = WebAPI.FormData.make()
let blob = WebAPI.Blob.make(~blobParts=[])
let stringBody = WebAPI.BodyInit.fromString("plain text")
let paramsBody = WebAPI.BodyInit.fromURLSearchParams(params)
let formDataBody = WebAPI.BodyInit.fromFormData(formData)
let blobBody = WebAPI.BodyInit.fromBlob(blob)

Use WebAPI.HeadersInit when a fetch, request, or response initializer expects the JavaScript HeadersInit shape. Use WebAPI.Headers when you want a mutable Headers object with methods.

let initHeaders = WebAPI.HeadersInit.fromDict(
dict{
"Accept": "application/json",
"X-Client": "web",
},
)
let response = await WebAPI.Fetch.fetch(
"https://example.com/api/users",
~init={headers: initHeaders},
)

Build and edit a Headers object directly when you need to append, set, inspect, or pass headers around.

let headers = WebAPI.Headers.fromDict(dict{"Accept": "application/json"})
headers->WebAPI.Headers.set(~name="X-Request-ID", ~value="abc-123")
headers->WebAPI.Headers.append(~name="X-Trace", ~value="enabled")
switch headers->WebAPI.Headers.get("Accept") {
| Null.Value(value) => Console.log(value)
| Null => Console.log("missing")
}
let hasTrace = headers->WebAPI.Headers.has("X-Trace")

Headers can also be created from key-value arrays or copied from another Headers object.

let fromPairs = WebAPI.Headers.fromKeyValueArray([
("Accept", "application/json"),
("X-Client", "web"),
])
let clonedHeaders = WebAPI.Headers.fromHeaders(fromPairs)
let initFromHeaders = WebAPI.HeadersInit.fromHeaders(clonedHeaders)

Response headers are normal Headers values.

let contentType = response.headers->WebAPI.Headers.get("Content-Type")
switch contentType {
| Null.Value(value) => Console.log(value)
| Null => Console.log("unknown content type")
}

Create requests from URLs, then pass them to fetch or clone/read them with request methods.

let req = WebAPI.Request.fromURL(
"https://example.com/form",
~init={
method: "POST",
headers: WebAPI.HeadersInit.fromDict(
dict{"Content-Type": "application/x-www-form-urlencoded"},
),
body: WebAPI.BodyInit.fromString("name=ReScript"),
},
)
let response = await WebAPI.Fetch.fetchWithRequest(req)
let cloned: WebAPI.Request.t = req->WebAPI.Request.clone
let bodyText = await cloned->WebAPI.Request.text

Use fromRequest to copy an existing request and override initializer fields.

let authenticatedHeaders = WebAPI.HeadersInit.fromDict(
dict{
"Accept": "application/json",
"Authorization": "Bearer token",
},
)
let authenticatedReq = WebAPI.Request.fromRequest(
req,
~init={headers: authenticatedHeaders},
)
let authenticatedResponse = await WebAPI.Fetch.fetchWithRequest(authenticatedReq)

Request body readers mirror the browser API and return promises.

let requestText = await req->WebAPI.Request.text
let requestJson = await req->WebAPI.Request.json
let requestBlob = await req->WebAPI.Request.blob

Construct responses with the body constructor that matches the body value.

let created = WebAPI.Response.fromString(
"created",
~init={
status: 201,
statusText: "Created",
headers: WebAPI.HeadersInit.fromDict(dict{"Content-Type": "text/plain"}),
},
)
let empty = WebAPI.Response.fromNull(~init={status: 204})

Responses expose record fields for status metadata and methods for cloning or reading the body.

if created.ok {
let text = await created->WebAPI.Response.text
Console.log(text)
} else {
Console.log(created.statusText)
}
let copy = created->WebAPI.Response.clone
let copyText = await copy->WebAPI.Response.text

Use the static helpers for browser response factories.

let failed = WebAPI.Response.error()
let redirect = WebAPI.Response.redirect(~url="/login", ~status=302)

DOM values are operated on through public interface modules.

let document = WebAPI.DOM.window->WebAPI.Window.document
let maybeButton = document
->WebAPI.Document.querySelector("button")
->Null.toOption
switch maybeButton {
| Some(button) =>
switch button->WebAPI.Element.getAttribute("data-user-id") {
| Null.Value(id) => Console.log(id)
| Null => Console.log("anonymous")
}
| None => Console.log("button not found")
}

Use conversion helpers when moving between related DOM interface types.

let element = document->WebAPI.Document.createElement("div")
let node = element->WebAPI.Element.asNode

Event libraries and browser APIs sometimes return a type that is broader than the value present at runtime. A React form event target, for example, does not tell ReScript whether the target is an input, select, or another element. Calling an input-specific API then requires narrowing that value.

Use classify instead of repeating an instanceof binding and an unchecked Obj.magic cast. The classifier performs the runtime check and returns the correctly typed value as an option.

let readInputValue = target =>
switch target->WebAPI.HTMLInputElement.classify {
| Some(input) => Some(input.value)
| None => None
}

The classifier parameter is polymorphic, so a library-owned target can be passed directly without an identity cast to a WebAPI type first. Element.classify, Document.classify, HTMLElement.classify, HTMLInputElement.classify, HTMLImageElement.classify, and HTMLCanvasElement.classify follow the same contract. Their matching isInstanceOf functions are available when only a boolean check is needed.

Classification uses the corresponding constructor from globalThis. It returns None rather than raising when that constructor is unavailable in a server or worker environment. Like JavaScript instanceof, it only recognizes values created in the current realm; values from another window or iframe may not match. Enable the WebAPI.HTML feature for HTML-specific classifiers and WebAPI.Canvas for HTMLCanvasElement.classify.

Window.visualViewport returns a nullable WebAPI.VisualViewport.t.

let maybeViewport = WebAPI.DOM.window
->WebAPI.Window.visualViewport
->Null.toOption
switch maybeViewport {
| Some(viewport) =>
Console.log(Float.toString(viewport.width))
Console.log(Float.toString(viewport.scale))
| None => Console.log("visual viewport unavailable")
}

URL helpers expose constructors and static functions on the same public module.

let url = WebAPI.URL.make(~url="/docs", ~base="https://rescript-lang.org")
let canParse = WebAPI.URL.canParse(~url="/docs", ~base="https://rescript-lang.org")
let json = url->WebAPI.URL.toJSON

URLSearchParams values can be used directly as fetch request bodies through BodyInit.

let params = WebAPI.URLSearchParams.fromString("search=rescript")
let response = await WebAPI.Fetch.fetch(
"https://example.com/search",
~init={
method: "POST",
body: WebAPI.BodyInit.fromURLSearchParams(params),
},
)

Canvas constructors use make and semantic from* names.

let path = WebAPI.Path2D.make()
path->WebAPI.Path2D.moveTo(~x=0., ~y=0.)
path->WebAPI.Path2D.lineTo(~x=100., ~y=100.)
let copiedPath = WebAPI.Path2D.fromPath2D(path)
let svgPath = WebAPI.Path2D.fromString("M0 0 L10 10")

Generic Web API types stay typed through their public owner modules.

let stream: WebAPI.ReadableStream.t<string> = WebAPI.ReadableStream.make()
let reader = stream->WebAPI.ReadableStream.getReader