urbex
v2.0.0
Published
A fast HTTP client built for NodeJS and the browser. Supports custom pipline transformations, isolated clients and more.
Downloads
6
Maintainers
Readme
urbex
An efficient and lightweight functional HTTP client for Node.js and the browser with built in cache support, customizable pipelines, isolated clients, and more.
Table of Contents
- Features
- Installation
- Usage
- Examples
- API Design
- Configuring the client
- Isolated Clients
- Pipeline Transformers
- Internal Cache Module
- Error Handling
- Caveats
- TypeScript
- Change Log
- License
Features
- Built for Node.js (http module) and the browser (xhr)
- Global configurations
- Isolated clients
- Built in client-side cache support
- Customizable pipelines for request and response transformations
- Custom status validation
- Ease of use API design
- Extended configuration when defining urls
- Subscribable requests for realtime updates (coming soon)
Installation
npm
$ npm install urbex
yarn
$ yarn add urbex
unpkg
<script src="https://unpkg.com/urbex"></script>
jsDelivr
<script src="https://cdn.jsdelivr.net/npm/urbex"></script>
Usage
ES6
import urbex from "urbex";
CommonJS
const urbex = require("urbex").default;
urbex.configure({});
Browser
For browser builds that are required to be loaded via a script tag, the urbex
global is exposed and is also available on the window
object.
<script src="https://unpkg.com/urbex"></script>
<script>
urbex.configure({});
// or
window.urbex.configure({});
</script>
The API is the same as module builds, with the exception of named exports. These are instead attached to the urbex
object.
Examples
Basic
import urbex from "urbex";
// configure the client to your liking
urbex.configure({})
// make a request
try {
const response = await urbex.get("https://jsonplaceholder.typicode.com/todos/1");
} catch (error) {
console.log(error)
}
All method aliases are supported for common HTTP verbs:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- OPTIONS
If you find yourself needing to use another method that doesn't have an alias, you can use the send
method instead.
const response = await urbex.send({
method: "PROPFIND",
url: "https://jsonplaceholder.typicode.com/todos/1"
});
Global Configuration
The API provides a global configuration that can be used to set the default options for all requests that are made. You are also granted the ability to override these options on a per-request basis.
urbex.configure({
url: "https://jsonplaceholder.typicode.com",
headers: {
"My-Custom-Header": "foo"
}
})
const response = await urbex.get("/todos/1", {
headers: {
"My-Custom-Header": "bar"
}
});
At any time you can check the current configuration with urbex.config
. This will return the current global configuration.
See Configuring the client for more information.
API Design
The API is designed to be as simple as possible. The goal is to provide a simple and easy to use API that is also flexible enough to be used in a variety of different scenarios.
urbex.send([, config])
Is the base method that all other methods are built on top of. This method accepts a configuration object that is used to make the request. The configuration object is then passed through a pipeline of transformers that are used to transform the request, make the request, and transform the response. By default, if no configuration is passed, the global configuration is used. Likewise, if a configuration is passed, it is merged with the global configuration.
Note: On each request, the global configuration is cloned and merged with the request configuration. This ensures that the global configuration is not mutated.
Verb Aliases
urbex.get(url[, config])
urbex.post(url[, data[, config]])
urbex.put(url[, data[, config]])
urbex.patch(url[, data[, config]])
urbex.delete(url[, config])
urbex.head(url[, config])
urbex.options(url[, config])
It is recommended if you find yourself only requesting data from the same URL with the same method, to use urbex.send
instead.
Configuring the client
The client can be configured globally or on a per-request basis. The global configuration is the default configuration that is used for all requests. The per-request configuration is used to override the global configuration for a specific request. You are freely able to use both the global and per-request configuration at the same time, along with calling .configure()
multiple times. All configurations are merged together in the order they are called.
The core detects what environment it is running in and will automatically set the default configuration for that environment.
| Property | Default | Description |
|----------------------|----------------------|----------------------------------------------------------------------------------------|
| url | Based on environment | The url to use for the request. Can be either a string or an object. |
| headers | Based on environment | Custom headers to be sent with the request. |
| method | GET
| The method of the request. |
| data | null
| Data to use when a request accepts a request body. |
| timeout | 0
| Set the default timeout to use for all requests. |
| cache | {}
| Control the internal ttl cache module. |
| pipelines | Based on environment | Custom pipeline transformers to use when making requests. |
| maxContentLength | Infinity
| The max content length of a response. |
| responseType | JSON
| The response type of the request. |
| responseEncoding | utf8
| The encoding to use when converting from a buffer to a string. |
| resolveStatus | >= 200 && < 300 | A function that determines whether the request should be considered successful or not. |
For clarification on the properties, check out the TypeScript Definitions.
Environment Defaults
Defaults are applied to the request configuration depending on what environment the client is running in. It is detected as either browser or node.
Browser
{
url: {
href: window.location.href,
origin: window.location.origin,
protocol: window.location.protocol,
username: "",
password: "",
hostname: window.location.hostname,
port: window.location.port,
pathname: window.location.pathname,
search: window.location.search,
hash: window.location.hash,
},
headers: {
"Content-Type": "application/json"
},
pipelines: {
request: [transformRequestData],
response: [transformResponseData]
}
}
Node
{
url: {
href: "http://localhost:3000",
origin: "http://localhost:3000",
protocol: "http",
username: "",
password: "",
hostname: "localhost",
port: 3000,
pathname: "",
search: "",
hash: ""
},
headers: {
"Content-Type": "application/json",
"User-Agent": "UrbexClient (Node.js v16.13.1; win32)"
},
pipelines: {
request: [transformRequestData],
response: [decodeResponseData, transformResponseData]
}
}
See transformers for more information on the default pipelines.
Isolated Clients
The client can be used in a variety of different ways. The most common way is to use the default client that is exported from the package. However, you are also able to create isolated clients that are completely independent of each other. This is useful if you want to have multiple clients that are configured differently.
import urbex from "urbex";
const client = urbex.isolateClient(config);
// or
const client = new urbex.Client(config);
// or
const client = urbex.isolateClient();
client.configure(config)
You are still given full functionality of the client and all vital methods are available, except the following:
isolateClient()
isUrbexClient()
Client
environment
If you wish to still use these methods, they are accessible through the urbex
object.
Note: For every isolated client that is instantiated, a new instance of the CacheClock
class is created. This is to ensure that each client has its own internal cache. If you enable the cache, remember to stop/disable the clock when you are done with the client.
Pipeline Transformers
Pipeline transformers allow you to take control over how the configuration is managed throughout the life-cycle of a request. Both request and response pipelines are supported. The request pipeline is used to transform the request configuration before the request is made. The response pipeline is used to transform the response data before it is returned to the user.
There are mandatory pipelines that are evaluated at runtime. These are vital to the core functionality of the client, such as setting appropriate headers, decoding response data, and automatic JSON parsing. See transformers for more information on the default pipelines.
You can create your own pipelines to use with the client. These can either be registered with the global configuration, or on a per-request basis. The pipelines are executed in the order they are registered. Each pipeline is asynchronous and must return a promise. The promise must resolve with the configuration object that was passed. Additionally, you are free to throw errors within the pipeline. These are handled automatically and will be rejected with the error.
Each pipeline is passed the configuration object after it has been transformed by the previous pipeline.
import urbex, { PipelineExecutor, RequestExecutor, ResponseExecutor } from "urbex";
const requestPipeline = new PipelineExecutor<RequestExecutor>((config) => {
// Do something with the request configuration
return Promise.resolve(config)
// return Promise.reject(new Error("Something went wrong in the request pipeline"))
})
const responsePipeline = new PipelineExecutor<ResponseExecutor>((config) => {
// Do something with the response configuration
return Promise.resolve(config)
// return Promise.reject(new Error("Something went wrong in the resposne pipeline"))
})
// both pipelines are executed AFTER the default pipelines
urbex.configure({
pipelines: {
request: [requestPipeline],
response: [responsePipeline]
}
})
// or
urbex.get("/users", {
pipelines: {
request: [requestPipeline],
response: [responsePipeline]
}
})
Pipeline executors can be globally registered, or on a per-request basis. Responses that are pulled from the internal cache module still go through the response pipeline. This is to ensure that the response is transformed correctly. Each PipelineExecutor
instance only registers one pipeline. If you wish to register multiple pipelines, you must create multiple instances of the PipelineExecutor
class. There is no limit to the number of pipelines that can be registered.
Whilst a simple integration, this is a powerful feature that allows you to perform any logic/operation before and after a request has been made.
Examples of pipelines that you can create:
- Automatically log requests
- Append a token to the request headers
- Modify the request URL based on a value
- Edit or set request body data
- Make a request to a different URL based on a condition
Injecting Pipelines
Coming Soon
An alternative method is to use injectPipeline(pipeline[, index])
to inject a pipeline into the existing pipeline chain. This is useful to control which position your pipeline is executed in. The index is optional and defaults to the end of the pipeline chain.
This gives you control to inject pipelines that are executed before the default pipelines, but we do not recommend this as it may cause unexpected behaviour.
Ejecting Pipelines
Coming Soon
You can also eject a pipeline from the pipeline chain using ejectPipeline(pipeline)
. This will remove the pipeline from the chain and return the pipeline that was removed.
Internal Cache Module
All requests, if configured, are cached internally. This is done to reduce the number of requests that are made to the server. The cache is based on the request configuration and the response data. The cache is cleared when the client is stopped. Before a request is made, the cache is checked to see if the request has been made before. If it has, the cached response is returned. If it has not, the request is made and the response is cached. The href of the request is used as the key for the cache. The raw result from the server request is cached, not the transformed response.
All pipelines are executed even if the request is cached. You may control this behaviour by using the cache
option in the request configuration.
urbex.configure({
cache: {
enabled: true,
ttl: 10000,
onExpire(entry) {
console.log(`${entry.k} has expired`);
}
}
})
const response = await urbex.get("https://jsonplaceholder.typicode.com/users");
console.log(response.cache) // { key: "", hit: true, pulled: false, stored: true };
const response2 = await urbex.get("https://jsonplaceholder.typicode.com/users");
console.log(response2.cache) // { key: "", hit: true, pulled: true, stored: false };
The urbex client also provides an accessor to the cache. This is useful if you want programmatic access to the cache. You are still able to use the cache directly without making requests.
console.log(urbex.cache) // CacheClock { ... }
// set a value in the cache
urbex.cache.set("https://jsonplaceholder.typicode.com/users", { data: "test" }, { ttl: 10000, overwrite: false });
// start the clock
urbex.cache.start();
// stop the clock
urbex.cache.stop();
Check out the Cache Clock module for more information on the cache.
Error Handling
Errors are handled automatically by the client. After each request has finished, the response status is checked to see if it is allowed to resolve. By default, all status codes that fall between 200 and 299 are allowed to resolve. If the status code is not allowed, the response is rejected with an error. The error object contains the response object and the status code.
/**
* The base error class that gets thrown when a request fails.
*/
export interface UrbexErrorType {
/**
* The name of the error.
*/
name: string;
/**
* The status of the error.
*/
status: number;
/**
* The config object that was used to make the request.
*/
config: InternalConfiguration;
/**
* The request object that was used to make the request.
*/
request: any;
/**
* The response object that was returned from the request.
*/
response: UrbexResponse;
/**
* The error message.
*/
message: string;
}
Additional errors such as NetworkError
, TimeoutError
and PipelineError
are thrown when the request fails depending on the error type. These classes along with the UrbexError
base class are available as a named export.
See the Urbex Error class for more information.
Resolve Status Codes
To control if a request is allowed to resolve, you can use the resolveStatus
option in the configuration. Both the current configuration and the response status code are passed to the function. The function must return a boolean value. Errors are caught automatically if this function were to throw an error.
urbex.get("https://jsonplaceholder.typicode.com/users", {
resolveStatus: (config, status) => {
// the internal config is also passed in case you wish to use it
return status === 200;
}
})
Caveats
Headers
There is one exception when setting the Content-Type
property on the headers
object. The built in transformRequestData
pipeline automatically ensures that POST
, PUT
and PATCH
requests have a Content-Type
header set to application/x-www-form-urlencoded
, and validates the request body if one is provided and sets the appropriate Content-Type
header.
To solve this, you will need to create a request pipeline that changes this header to whatever you require it to be.
import urbex, { PipelineExecutor } from "urbex";
urbex.configure({
headers: {
"Content-Type": "multipart/form-data" // will get overwritten for POST, PUT and PATCH requests
}
})
const response = await urbex.post("https://jsonplaceholder.typicode.com/users");
response.config.headers["Content-Type"] // application/x-www-form-urlencoded
// solution
urbex.configure({
pipelines: {
request: [
new PipelineExecutor((config) => {
config.headers.set({
"Content-Type": "multipart/form-data"
})
return Promise.resolve(config)
})
]
}
})
// now on each request, the header will be set to multipart/form-data
const response = await urbex.send();
response.config.headers["Content-Type"]; // multipart/form-data
This only applies to the Content-Type
header.
TypeScript
Urbex is written in TypeScript and includes type definitions, along with type guards. Below are details on the types that are exported.
import urbex from "urbex";
const client = urbex.isolateClient(config);
if (urbex.isUrbexClient(client)) {
// client is a UrbexClient
}
Interfaces and Types
import {
ExtendedUrbexClient,
InternalConfiguration,
PipelineExecutor,
RequestExecutor,
ResponseExecutor,
URIComponent,
UrbexConfig,
UrbexErrorType,
UrbexResponse,
UrbexURL
} from "urbex";
For more information on the types, see the Urbex Types file.
Change Log
See CHANGELOG.md
License
This project is licensed under the MIT License - see the LICENSE file for details