zoxios
v0.1.12
Published
A chainable validated HTTP request maker
Downloads
1,935
Maintainers
Readme
Features
- Parsing HTTP requests' body and query parameters and responses' body using Zod
- Chainable API.
- Make HTTP requests using axios
installation
Install peer dependencies
npm i axios zod
Install zoxios
npm i zoxios
Basic usage
Parsing request's query and response's body.
import { zoxios } from 'zoxios';
// GET http://hostname/api/orders?page=1&limit=10
const response = await zoxios('http://hostname/api/orders')
.method('GET')
.querySchema(z.object({ page: z.number(), limit: z.number() }))
.query({ page: 1, limit: 10 }) // { page: number; limit: number; }
.responseSchema(z.array(z.object({ id: z.number() })))
.exec();
// response: { id: number; }[]
console.log(response);
// [ { id: 1 }, { id: 2 } ]
Without Parsing
Requests can be made without defining parsing schemas for the request and the response.
// GET http://hostname/api/orders?page=1&limit=10
const response = await zoxios('http://hostname/api/orders')
.method('GET')
.query({ page: 1, limit: 10 }) // unknown
.exec();
// response: unknown
console.log(response);
// [ { id: 1 }, { id: 2 } ]
Reusing and adding on-top of the request-maker
The request-maker is chainable, therefore it can help minimize code repetition by defining reusable "api layers".
Example
Every request maker built on top on this one will have the same host, headers and the /api path
function getBaseRequestMaker() {
const options = { headers: { Authorization: `Bearer token` } };
return zoxios('http://hostname')
.options(options)
.concatPath('api');
}
Every request maker built on top of this one will include the settings from getBaseRequestMaker
and the /orders
path
function buildOrdersRequestMaker() {
return getBaseRequestMaker().concatPath('orders');
}
This function will make a request including all of the settings from buildOrdersRequestMaker
and:
- set the HTTP method as POST
- parse the specified body with the provided bodySchema.
- parse the response with the provided responseSchema
function createOrder(itemId: string, amount: number) {
return buildOrdersRequestMaker()
.method('post')
.bodySchema(z.object({ itemId: z.string(), amount: z.number() }))
.responseSchema(z.object({ id: z.number() }))
.body({ itemId, amount })
.exec(); // Promise<{ id: number }>
}
Every request maker built on top on this one will include the settings from buildOrdersRequestMaker
and the settings from this maker:
- A GET HTTP method
- A response schema definition
function buildGetOrdersRequestMaker() {
return buildOrdersRequestMaker()
.method('get')
.responseSchema(z.array(z.object({ id: z.number() })));
}
This function will make a request including all of the settings from buildGetOrdersRequestMaker
and parse the specified query with the provided querySchema.
function getOrdersPaginated(page: number, limit: number) {
return buildGetOrdersRequestMaker()
.querySchema(z.object({ page: z.number(), limit: z.number() }))
.query({ page, limit }) // { page: number; limit: number; }
.exec(); // Promise<{ id: number }[]
}
This function will make a request including all of the settings from buildGetOrdersRequestMaker
and parse the specified query with the provided querySchema.
function getOrdersInDateRange(startDate: Date, endDate: Date) {
return buildGetOrdersRequestMaker()
.querySchema(z.object({ startDate: z.date(), endDate: z.date() }))
.query({ startDate, endDate }) // { startDate: Date; endDate: Date; }
.exec(); // Promise<{ id: number }[]
}
.options
Set axios options.
Every options set here will be overridden if set again in later chains, by calling options
or other method resetting the value you defined here.
zoxios('http://hostname').options({ timeout: 1000 });
.asyncOptionsSetter
Set axios options asynchronously. Provide an async function that will return axios options object. This async function will run before each request and set the options. Other set options will have priority over this one (only relevant when same option property is set).
Can be useful when each request have to calculate a request-signature or a token asynchronously.
zoxios('http://hostname')
.asyncOptionsSetter(async () => ({ headers: { Authorization: await Promise.resolve('token') } }))
.concatPath
Will concat to the path defined up until its usage.
Each concatPath adds a "/
" before the provided value.
zoxios('http://hostname')
.concatPath('api') // current url - http://hostname/api
.concatPath('users') // current url - http://hostname/api/users
.concatPath(5); // current url - http://hostname/api/users/5
This example will create the following URL:
http://hostname/api/users/5
.getDefinition
Will return the definition of the request-maker which will include the hostname, querySchema, bodySchema, responseSchema, query, body, path, options and method.
const body = { name: 'n', age: 1 };
const responseSchema = z.object({ id: z.number() });
const query = { endDate: new Date(), startDate: new Date() };
const bodySchema = z.object({ name: z.string(), age: z.number() });
const querySchema = z.object({ startDate: z.date(), endDate: z.date() });
const requestMaker = zoxios('localhost')
.concatPath('api')
.concatPath('orders')
.querySchema(querySchema)
.bodySchema(bodySchema)
.responseSchema(responseSchema)
.body(body)
.query(query);
const definition = requestMaker.getDefinition();
// definition.body = body;
// definition.query = query;
// definition.path = '/api/orders';
// definition.hostname = 'localhost';
// definition.bodySchema = bodySchema;
// definition.querySchema = querySchema;
// definition.responseSchema = responseSchema;
.host
Will change the host of the request-maker.
const requestMaker = zoxios('https://localhost-1').concatPath('api').method('get');
// GET https://localhost-1/api
const validatedRequestResponse1 = await requestMaker.exec();
// GET https://localhost-2/api
const validatedRequestResponse2 = await requestMaker.host('https://localhost-2').exec();
.handleHttpError
Will add an http error handler that will handle errors thrown by axios, you should check if the provided error is an axios error (using isAxiosError) if you wish to handle those explicitly.
The error-handlers you define will ran one after the other: each one will pass the error to the next error-handler in-line, if an error exists, otherwise it will resolve the promise and ignore the rest of the error-handlers.
example for one error handler:
// instead of throwing an error, null will be returned.
await zoxios('https://localhost')
.concatPath('api')
.method('get')
.handleHttpError((error) => {
return null;
})
.exec()
example for multiple error handlers:
// instead of the original error, the new error created will be thrown.
await zoxios('https://localhost')
.concatPath('api')
.method('get')
.handleHttpError((error) => {
console.log(error);
throw error;
})
.handleHttpError(() => {
throw new Error('other error');
})
.exec()