Keq is a request API write by Typescript for flexibility, readability, and a low learning curve. It also works with Node.js! Keq wraps the Fetch APIs, adding chain calls and middleware functions.
A request can be initiated by invoking the appropriate method on the request object,
then calling .then()
(or .end()
or await
) to send the request.
For example a simple GET request:
import { request } from "keq";
const body = await request
.get("/search")
.set("X-Origin-Host", "https://example.com")
.query("key1", "value1");
Request can be initiated by:
import { request } from "keq";
const body = await request({
url: "/search",
method: "get",
});
Absolute URLs can be used. In web browsers absolute URLs work only if the server implements CORS.
import { request } from "keq";
const body = await request.get("https://example.com/search");
DELETE, HEAD, PATCH, POST, and PUT requests can also be used, simply change the method name:
import { request } from "keq";
await request.head("https://example.com/search");
await request.patch("https://example.com/search");
await request.post("https://example.com/search");
await request.put("https://example.com/search");
await request.delete("https://example.com/search");
await request.del("https://example.com/search");
.del()
is the alias of.delete()
.
Keq
will parse body
according to the Content-Type
of Response
and return undefined
if Content-Type
not found.
Add invoke .resolveWith('response')
to get the origin Response
Object.
import { request } from "keq";
const response = await request
.get("http://test.com")
.resolve('response')
const body = await response.json();
We will introduce resolveWith
in more detail later.
Keq
won't auto parse body, if response.status is 204. The HTTP 204 No Content success status response code indicates that server has fulfilled the request but does not need to return an entity-body, and might want to return updated meta information
Setting header fields is simple, invoke .set()
with a field name and value:
import { request } from "keq";
await request
.get("/search")
.set("X-Origin-Host", "https://example.com")
.set("Accept", "application/json");
You may also pass an object or Headers
to set several fields in a single call:
import { request } from "keq";
await request
.get("/search")
.set({
"X-Origin-Host": "https://example.com",
Accept: "application/json",
});
The .query()
method accepts objects,
which when used with the GET method will form a query-string.
The following will produce the path /search?query=Manny&range=1..5&order=desc.
import { request } from "keq";
await request
.get("/search")
.query({ query: "Manny" })
.query({ range: "1..5" })
.query("order", "desc");
Or as a single object:
import { request } from "keq";
await request
.get("/search")
.query({ query: "Manny", range: "1..5", order: "desc" });
The .params()
method accepts key and value, which when used for the request with routing parameters.
import { request } from "keq";
await request
// request to /users/jack/books/kafka
.get("/users/:userName/books/{bookName}")
.params("userName", 'jack');
.params("bookName", "kafka");
// or invoke with an object
.params({
"userName": "jack",
"bookName": "kafka"
})
A typical JSON POST request might look a little like the following,
where we set the Content-Type
header field appropriately:
import { request } from "keq";
await request
.post("/user")
.set("Content-Type", "application/json")
.send({ name: "tj", pet: "tobi" });
When passed an object
to .send()
, it will auto set Content-Type
to application/json
A typical Form POST request might look a little like the following:
import { request } from "keq";
await request
.post("/user")
.type("form")
.send({ name: "tj", pet: "tobi" })
.send("pet=tobi");
To send the data as application/x-www-form-urlencoded
simply invoke .type()
with "form".
When passed an string
to .send()
, it will auto set Content-Type
to application/x-www-form-urlencoded
.
When calling
.send ()
multiple times, the value ofContent-Type
will only be set when the first calling.send ()
.
A typical Form POST request might look a little like the following:
import { request } from "keq";
const form = new FormData();
form.append("name", "tj");
form.append("pet", "tobi");
// prettier-ignore
await request
.post("/user")
.type("form-data")
.send(form)
When passed an FormData
object to .send()
, it will auto set Content-Type
to multipart/form-data
.
You can append field by invoke .field()
and .attach()
import { request } from "keq";
await request
.post("/user")
.field("name", "tj")
.field("pet", "tobi")
.attach("file", new Blob(["I am tj"]));
The obvious solution is to use the .set() method:
import { request } from "keq";
// prettier-ignore
await request
.post("/user")
.set("Content-Type", "application/json")
As a short-hand the .type() method is also available, accepting the canonicalized MIME type name complete with type/subtype, or simply the extension name such as "xml", "json", "png", etc:
import { request } from "keq";
await request
.post("/user")
.type("json");
Shorthand | Mime Type |
---|---|
json, xml | application/json, application/xml |
form | application/x-www-form-urlencoded |
html, css | text/html, text/css |
form-data | multipart/form-data |
jpeg, bmp, apng, gif, x-icon, png, webp, tiff | image/jpeg, image/bmp, image/apng, image/gif, image/x-icon, image/png, image/webp, image/tiff |
svg | image/svg+xml |
It was mentioned before that Keq
will automatically parses the response body.
And we can control the parsing behavior by calling .resolveWith(method)
.
There are multiple parsing methods for us to choose from
method | description |
---|---|
.resolveWith('intelligent') |
It is the default method of Keq . This will returned context.output first if it exists. Otherwise return undefined when the response status is 204. Or return parsed response body according to the Content-Type of Response . |
.resolveWith('response') |
Return Response . |
.resolveWith('text') |
Return response.text() . |
.resolveWith('json') |
Return response.json() . |
.resolveWith('form-data') |
Return response.formData() . |
.resolveWith('blob') |
Return response.blob() . |
.resolveWith('array-buffer') |
Return response.arrayBuffer() |
No retry by default, invoke .retry(retryTimes[, retryDelay[, retryOn]])
to set retry parameters
Parameter | Default | Description |
---|---|---|
retryTimes | 0 |
Max number of retries per call. |
retryDelay | 0 |
Initial value used to calculate the retry in milliseconds (This is still randomized following the randomization factor). |
retryOn | (attempt, error) => !!error |
Will be called after request used to control whether the next retry runs. If it return false , stop retrying. |
import { request } from "keq";
await request
.get("http://test.com")
.retry(2, 1000, (attempt, err, ctx) => {
if (err) {
console.log("an error throw");
return true;
}
return false;
});
Follow redirect by default, invoke .redirect(mode)
to set the redirect mode. Allow values are "error"
, "manual"
and "follow"
.
import { request } from "keq";
await request
.get("http://test.com")
.redirect("manual");
These two parameters are used to control cross-domain requests.
import { request } from "keq";
await request
.get("http://test.com")
.mode("cors")
.credentials("include");
Invoke .option()
add options.
import { request } from "keq";
const response = await request
.get("http://test.com")
.option("middlewareOption", "value");
Or as a single object:
import { request } from "keq";
await request
.get("http://test.com")
.options({
middlewareOption: "value",
});
Option | Description |
---|---|
fetchAPI |
Replace the defaulted fetch function used by Keq . |
Keq has built-in timeout function.
await request
.get("http://test.com")
// 5000 milliseconds
.timeout(5000)
Controlling the behavior of sending requests multiple times.
If the previous request was not completed, abort the last request.
import { request } from "keq";
request
.get("http://test.com/cat")
// second args is the abort signal
// this will abort the request with same signal
// a unique signal will be generated, if not signal set.
.followControl("abort", 'animal');
.end()
request
.get("http://test.com/dog")
// abort http://test.com/cat
.followControl("abort", 'animal')
.end()
The next request will not start until the previous request is completed.
import { request } from "keq";
request
.get("http://test.com/cat")
// a unique signal will be generated, if signal is not set.
.followControl("serial", 'animal');
.end()
// This request will be send after https://test.com/cat is complete
request
.get("http://test.com/dog")
.followControl("serial", 'animal')
.end()
You can extend Keq
by write/import middleware.
A typical middleware might look a little like the following:
import { request } from "keq"
const middleware = async (context, next) => {
// equal to .retry(2)
context.options.retryTimes = 2
await next()
const response = context.response
if (!response) return
const body = await response.json()
// custom keq return type
context.output = JSON.stringify(body)
}
// Global Middleware
request
.use(middleware)
request
.useRouter()
/**
* the middleware run when request url host is "example.com"
*/
.host("example.com", middleware)
/**
* the middleware run when request url is location
* It is usefully in browser.
*/
.location(middleware)
/**
* the middleware run when pathname match `/api/service_name/**`.
*/
.pathname("/api/service_name/**" middleware)
/**
* the middleware run when method is GET
*/
.method('get', middleware)
/**
* used with keq-cli
*/
.module('yourServiceName',middleware)
/**
* this middleware run when pathname start with '/api'
*/
.route((ctx) => ctx.pathname.startsWith('/api'), middleware)
await request
.get("http://test.com")
/**
* the middleware run once
*/
.use(middleware)
Add an global middleware, The running order of middleware is related to the order of .use()
Middleware Router
Middleware should be an asnyc-function that accept two argument:
Arguments | Description |
---|---|
ctx (first argument) |
Keq Context |
next (second argument) |
Used to execute the next middleware. The last next() function will send request and bind the Response object to context.res . Don't forget to call next() unless you don't want to send the request. |
Keq's context object has many parameters. The following lists all the built-in context attributes of Keq
:
Property | Type |
---|---|
context.request |
Includes request options for Fetch API. |
context.request.url |
URL Class |
context.request.__url__ |
Readonly URL Class that merged routeParams |
context.request.method |
One of 'get', 'post', 'put', 'patch', 'head', 'delete'. |
context.request.body |
Object, Array, string or undefined. |
context.request.headers |
The Headers Object. |
context.request.routeParams |
The URL route params set by .params(key, value) |
context.request.catch |
catch arguments in Fetch API |
context.request.credentials |
credentials arguments in Fetch API |
context.request.integrity |
integrity arguments in Fetch API |
context.request.keepalive |
keepalive arguments in Fetch API |
context.request.mode |
mode arguments in Fetch API |
context.request.redirect |
redirect arguments in Fetch API |
context.request.referrer |
referrer arguments in Fetch API |
context.request.referrerPolicy |
referrerPolicy arguments in Fetch API |
context.request.signal |
signal arguments in Fetch API |
context.options |
It is an object includes request options.(example: context.options.fetchAPI ). Middleware can get custom options from here. |
context.res |
The origin Response Class. It will be undefined before run await next() or error throwed. |
context.response |
Cloned from ctx.res . |
context.output |
Custom return value of await request() 。 It only take effect when resolveWith is not set or set to 'intelligent'. This property is writeonly. |
This is the utils used to route middleware.
Method |
---|
.location(...middlewares) |
.method(method: string[, ...middlewares]) |
.pathname(matcher: string | Regexp[, ...middlewares]) |
.host(host: string[, ...middlewares]) |
.module(moduleName: string[, ...middlewares]) |
.route(...middlewares) |
If you want to create a request instance, you can invoke request.create()
:
import { createRequest } from "keq";
const customRequest = createRequest();
// Middleware only takes effect on customRequests
customRequest.use(/** some middleware */);
const body = await customRequest.get("http://test.com");
The gloabl request instance is created by
request.create()
too.
option | description |
---|---|
initMiddlewares | fetch , retry , flowController are all implemented by middleware. you can customize the init middlewares to change behavior. |
baseOrigin | When sending a request without an origin , origin will set to window.location.origin in the browser and "http://127.0.0.1" in NodeJS. |
Both .then ()
and .end ()
will send a request and return a Promise object.
The difference between the two is that when called multiple times.
.then ()
actually sends only one request, no matter how many times it is called.
.end ()
will send a request for each call.
import { request } from "keq";
const keq = request.get("http://test.com");
keq.then(onfulfilled, onrejected);
// Won't send request, and will use the last request result.
keq.then(onfulfilled, onrejected);
keq.end();
keq.end();
ctx.response
will allways return a new Response
created by ctx.res && ctx.res.clone()
. Sothat each middleware could calling ctx.response.json()
, ctx.response.text()
, ctx.response.formData()
.
What's more, The .formData()
function isn't existed in Response
returned by node-fetch
. keq will append it to Response
after clone, if in NodeJS
.
Keq is inspired by SuperAgent and Koa.
If there is any doubt, it is very welcome to discuss the issue together.