Skip to content

rogerio-romao/tchef

Repository files navigation

Tchef

npm License Build Status JSR JSR

A better Fetch - with bells and whistles

Warning: this is a heavily wip package that is in initial stages, and also my first ever package. Use it at your own risk. I have made it for my own personal use, because at my work I am constantly writing fetch functions to grab data, and it got tiresome to do try-catch blocks all the time, and having to check for errors even in the try block. I also wanted the ability to have retries in case of error and to type the responses, so this package is the result of that.

It's meant to work on Node, other JS runtimes and browsers, just assumes a fairly recent version, since it assumes that it has native Fetch implementation. Recommended Node >=21, although it may work with earlier (>18) versions, but you may need to set a flag on Node. It's also a modern, ESM only module, no other exports provided. It is written in Typescript and ships with the types, so it's ready to get help from your IDE on using it and autocompletion.

Runtimes & Browser Compatibility

Compatible with: Node.js Deno Bun Cloudflare Workers

The package is published both on npm and jsr. Tested and compatible with the following:

  • Node.js
  • Deno
  • Bun
  • CloudflareWorkers

On browsers, it (kinda) works - it does it's job, just have to import it weirdly. For now, I haven't been able to have it run with a normal import in the browser. It works but with import tchef from './node_modules/tchef/dist/index.js'; instead of import tchef from 'tchef'. That will have to do for now, until I figure out the solution - suggestions welcome!

Installation & Basic Usage

# with pnpm - or use your preferred package manager
pnpm add tchef

In your project:

import tchef from 'tchef';

const res = await tchef('https://jsonplaceholder.typicode.com/posts/1');
console.log(res);

So, very simple and at the most basic, the same as using Fetch normaly. Except the return is a result type, and the call doesn't need to be wrapped on try/catch. If an error occurs, you get ok: false, a statusCode and an error message returned to you.

To use the JSR version of the package instead, follow the instructions here. It will still work on any of the compatible runtimes listed above.

Features

✔︎ Result type returns.

If the fetch is successful, you get an object with

{
    ok: true, data: T = unknown;
}

Otherwise, you get

{
    ok: false, error: string, statusCode: number;
}

So data will be whatever was the response when the call succeeded. A generic can be passed in to type the response data for you, otherwise it defaults to unknown. If it didn't succeed, the error will be a string message with different messages depending on the type of the error - 404, malformed url, network error, error parsing the json, etc.

That means calling Tchef should not need to be wrapped around a try-catch, instead it will always return and you only need to check for the ok property - if it's true, you are guaranteed to have data, if it is false, you are guaranteed to have a descriptive error message.

✔︎ Options.

Currently supports options for GET, POST, PUT, DELETE methods; for creating and sending headers, for json, text, blob response type and parsing, for creating and sending search params, and for selecting different types of caching.

Default options:

{
    method: 'GET',
    headers: {
        Accept: 'application/json',
    },
    responseFormat: 'json',
    cacheType: 'private',
    cacheMaxAge: 60,
    timeoutSecs: 'no-limit',
    retries: 0,
    retryDelayMs: 100,
};

✔︎ Timeout & Abort.

You can set optional timeout in seconds (default no time limit, meaning it will be handled by the browser or runtime). You can also optionally create an AbortController and send its signal using the signal option. Example of setting a timeout for 1 second:

 await tchef('https://httpbin.org/delay/2', { timeoutSecs: 1 });
 // that url will only reply after 2 seconds, so this will return:
 { ok: false, error: 'Request timeout', statusCode: 408 }

This way you can send an abort signal, that could be triggered by a button on your app instead of a setTimeout as in the example:

const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

await tchef('https://httpbin.org/delay/2', { signal: controller.signal });

✔︎ Retries.

You can set a number of retries for the cases when the fetch fails. By default it is set to 0. You can also select the amount of delay time in miliseconds to wait between the retries, by default 100ms. You can also set this option to the value 'exponential', and the delay time will increase exponentially with each retry, starting at 1sec, then 2, 4 and so on, up to your amount of retries. The error that happened on the previous attempt gets forwarded to the next attempt, and will be returned on the last attempt.

const result = await tchef('https://thisisfake.url', { retries: 2 });

if (!result.ok) {
    expect(result.error).toBe('Max retries reached. Not Found');
    expect(result.statusCode).toBe(404);
}

✔︎ Generic typing.

A type can be passed in to Tchef, that will be used to type the returned data. This is not the same as validating that the data is actually the correct type, it is just doing a typecast, so you get autocomplete and errors on the IDE when trying to access properties on the type. It is essentially the same as doing this:

type something = { some: string; thing: string };
const response = await fetch('https://some.website.com');
if (!response.ok) {
    // handle error
}
const data = (await response.json()) as something;

But since Tchef wraps around Fetch, we instead do that like this:

const response = await tchef<something>('https://some.website.com');

And the result is the same, the response is typed as something. If nothing is passed, the response will default to type unknown.

✔︎ Validating JSON payloads.

We use Valibot for validation. It's beyond the scope for us to explain how that works in detail, so follow the link for docs and guides. But it's a very lightweight and tree-shakeable library similar to Zod and others. By default Tchef will not validate payloads. To do that, you have to set up your expected schema using Valibot, then pass that in the options as such: { validateSchema: SomeValidValibotSchema }. A more detailed example here:

const TodoSchema = object({
    userId: number(),
    id: number(),
    title: string(),
    completed: boolean(),
});

const result = await tchef('https://jsonplaceholder.typicode.com/todos/1', {
    validateSchema: TodoSchema,
});

So, the only thing you need to have the payload validated is that. If you don't set the validateSchema property in the options, there is no validation. Although you can still use a generic as detailed above on this document.

So, there are 3 levels to the security you have about your response. At the most basic, with no validation or generic, the response data will be typed as unknown - already better than the any from a normal Fetch. By creating a TS type and passing it as a generic to the Tchef call, you get the response data type coerced into the expected type, with autocomplete and errors on IDE, but no real assurance that the data we got back is actually that type. By using the validation feature, you get the data correctly typed, and assurance that it was what you actually got back - at runtime! 🔥

Another bonus feature of the validation feature: if you expect the response to be a huge payload with dozens of fields, but you only really care about 5 or 6... you can create the schema with only those, and by default the response will still validate correctly (as long as those 5 or 6 were correct), and the return data will be stripped of the fields not in the schema, giving you back just what you need, a la GraphQL! 🔥

That is the default behaviour. You can also make it fail validation if there are more fields than your schema has, or still include those extra fields in the returned data instead of stripping them. The only change needed for this is on your Valibot schema. Example for including the extra fields:

const TodoSchema = object(
    {
        id: number(),
        title: string(),
    },
    unknown()
);

await tchef('https://example.com/todo/1', { validateSchema: TodoSchema });

The key is the unknown() call on the second argument - this will pass through the extra fields. If you set it to never() instead, validation will error out and tchef will return { ok: false, error: 'Response failed to validate against schema.', statusCode: 409 }. Again, check Valibot for more information on it's features.

Validation feature known issues:

  • If you get an error when trying to use the validation feature, that it cannot find Valibot, you can either try pnpm i --shamefully-hoist, that will rebuild your node_modules folder and pull up valibot from within Tchef's dependencies to the top level (recommended). Or you can install Valibot on your dependencies yourself - this should also work but you will have duplicated dependency and potential version mismatches.

Roadmap

  • Browser build that allows normal ESM import.

Releases

No releases published

Packages

No packages published