@viliaapro/apifetch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -0
- package/dist/index.d.mts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +72 -0
- package/dist/index.mjs +44 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# apifetch
|
|
2
|
+
|
|
3
|
+
A typed fetch wrapper with [Standard Schema](https://github.com/standard-schema/standard-schema) validation and status-code dispatch.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
HTTP responses fall into two categories:
|
|
8
|
+
|
|
9
|
+
- **User errors** are expected outcomes — a 404, a 422, a 401. These are part of the API contract and should be handled as returned values, not exceptions. The caller defines a schema and transform for each status code they care about.
|
|
10
|
+
- **Computer errors** are unexpected failures — the server returned a status code the client has no handler for, or the response body didn't match the expected schema. These indicate a bug or a contract violation and should throw exceptions.
|
|
11
|
+
|
|
12
|
+
This means `apiFetch` never throws for non-ok responses. A 422 Unprocessable Content is not an exception — it's a value you handle. Only truly unexpected situations — unrecognized status codes and malformed response bodies — throw.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @viliaapro/apifetch @standard-schema/spec http-status-codes zod
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { apiFetch } from 'apifetch';
|
|
24
|
+
import { StatusCodes } from 'http-status-codes';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
|
|
27
|
+
const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
28
|
+
const ErrorSchema = z.object({ message: z.string() });
|
|
29
|
+
|
|
30
|
+
const result = await apiFetch('/api/users/1', {
|
|
31
|
+
[StatusCodes.OK]: {
|
|
32
|
+
schema: UserSchema,
|
|
33
|
+
transform: (user) => ({ kind: 'ok' as const, user }),
|
|
34
|
+
},
|
|
35
|
+
[StatusCodes.NOT_FOUND]: {
|
|
36
|
+
schema: ErrorSchema,
|
|
37
|
+
transform: (error) => ({ kind: 'not_found' as const, error }),
|
|
38
|
+
},
|
|
39
|
+
[StatusCodes.UNPROCESSABLE_ENTITY]: {
|
|
40
|
+
schema: ErrorSchema,
|
|
41
|
+
transform: (error) => ({ kind: 'invalid' as const, error }),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
switch (result.kind) {
|
|
46
|
+
case 'ok': return result.user;
|
|
47
|
+
case 'not_found': return null;
|
|
48
|
+
case 'invalid': return showErrors(result.error);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Schema agnostic
|
|
53
|
+
|
|
54
|
+
`apiFetch` accepts any [Standard Schema](https://github.com/standard-schema/standard-schema) compliant validator — Zod, Valibot, ArkType, or any other compliant library.
|
|
55
|
+
|
|
56
|
+
## Errors
|
|
57
|
+
|
|
58
|
+
Two exceptions are thrown, both extending `ApiError`:
|
|
59
|
+
|
|
60
|
+
- **`UnrecognizedStatusError`** — the server returned a status code with no registered handler
|
|
61
|
+
- **`MalformedResponseError`** — the response body failed schema validation; carries `issues` with the validation details
|
|
62
|
+
|
|
63
|
+
Both carry the original `Response` object and a `status` convenience getter.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { UnrecognizedStatusError, MalformedResponseError } from 'apifetch';
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await apiFetch('/api/users/1', handlers);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (err instanceof MalformedResponseError) {
|
|
72
|
+
console.error(err.status, err.issues);
|
|
73
|
+
}
|
|
74
|
+
if (err instanceof UnrecognizedStatusError) {
|
|
75
|
+
console.error(err.status);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `apiFetch<R>(url, handlers, options?)`
|
|
83
|
+
|
|
84
|
+
| Parameter | Type | Description |
|
|
85
|
+
|------------|-----------------------|------------------------------------|
|
|
86
|
+
| `url` | `string` | Request URL |
|
|
87
|
+
| `handlers` | `ResponseHandlers<R>` | Map of status codes to handlers |
|
|
88
|
+
| `options` | `RequestInit` | Standard fetch options (optional) |
|
|
89
|
+
|
|
90
|
+
Returns `Promise<R>` where `R` is inferred from the transform return types.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
type StatusHandler<S extends StandardSchemaV1, R> = {
|
|
4
|
+
schema: S;
|
|
5
|
+
transform: (value: StandardSchemaV1.InferOutput<S>) => R;
|
|
6
|
+
};
|
|
7
|
+
type ResponseHandlers<R> = {
|
|
8
|
+
[S in number]?: StatusHandler<StandardSchemaV1, R>;
|
|
9
|
+
};
|
|
10
|
+
declare class ApiError extends Error {
|
|
11
|
+
readonly response?: Response | undefined;
|
|
12
|
+
get status(): number | undefined;
|
|
13
|
+
constructor(message: string, response?: Response | undefined, options?: ErrorOptions);
|
|
14
|
+
}
|
|
15
|
+
declare class UnrecognizedStatusError extends ApiError {
|
|
16
|
+
constructor(response: Response);
|
|
17
|
+
}
|
|
18
|
+
declare class MalformedResponseError extends ApiError {
|
|
19
|
+
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
20
|
+
constructor(response: Response, issues: readonly StandardSchemaV1.Issue[]);
|
|
21
|
+
}
|
|
22
|
+
declare function apiFetch<R>(url: string, handlers: ResponseHandlers<R>, options?: RequestInit): Promise<R>;
|
|
23
|
+
|
|
24
|
+
export { ApiError, MalformedResponseError, type ResponseHandlers, type StatusHandler, UnrecognizedStatusError, apiFetch };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
type StatusHandler<S extends StandardSchemaV1, R> = {
|
|
4
|
+
schema: S;
|
|
5
|
+
transform: (value: StandardSchemaV1.InferOutput<S>) => R;
|
|
6
|
+
};
|
|
7
|
+
type ResponseHandlers<R> = {
|
|
8
|
+
[S in number]?: StatusHandler<StandardSchemaV1, R>;
|
|
9
|
+
};
|
|
10
|
+
declare class ApiError extends Error {
|
|
11
|
+
readonly response?: Response | undefined;
|
|
12
|
+
get status(): number | undefined;
|
|
13
|
+
constructor(message: string, response?: Response | undefined, options?: ErrorOptions);
|
|
14
|
+
}
|
|
15
|
+
declare class UnrecognizedStatusError extends ApiError {
|
|
16
|
+
constructor(response: Response);
|
|
17
|
+
}
|
|
18
|
+
declare class MalformedResponseError extends ApiError {
|
|
19
|
+
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
20
|
+
constructor(response: Response, issues: readonly StandardSchemaV1.Issue[]);
|
|
21
|
+
}
|
|
22
|
+
declare function apiFetch<R>(url: string, handlers: ResponseHandlers<R>, options?: RequestInit): Promise<R>;
|
|
23
|
+
|
|
24
|
+
export { ApiError, MalformedResponseError, type ResponseHandlers, type StatusHandler, UnrecognizedStatusError, apiFetch };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiError: () => ApiError,
|
|
24
|
+
MalformedResponseError: () => MalformedResponseError,
|
|
25
|
+
UnrecognizedStatusError: () => UnrecognizedStatusError,
|
|
26
|
+
apiFetch: () => apiFetch
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var ApiError = class extends Error {
|
|
30
|
+
constructor(message, response, options) {
|
|
31
|
+
super(message, options);
|
|
32
|
+
this.response = response;
|
|
33
|
+
this.name = this.constructor.name;
|
|
34
|
+
}
|
|
35
|
+
get status() {
|
|
36
|
+
return this.response?.status;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var UnrecognizedStatusError = class extends ApiError {
|
|
40
|
+
constructor(response) {
|
|
41
|
+
super(`Unrecognized status code: ${response.status}`, response);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var MalformedResponseError = class extends ApiError {
|
|
45
|
+
constructor(response, issues) {
|
|
46
|
+
super(`Malformed response body for status: ${response.status}`, response);
|
|
47
|
+
this.issues = issues;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
async function apiFetch(url, handlers, options = {}) {
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
...options,
|
|
53
|
+
headers: { "Content-Type": "application/json", ...options.headers }
|
|
54
|
+
});
|
|
55
|
+
const handler = handlers[response.status];
|
|
56
|
+
if (!handler) {
|
|
57
|
+
throw new UnrecognizedStatusError(response);
|
|
58
|
+
}
|
|
59
|
+
const body = await response.json();
|
|
60
|
+
const result = await handler.schema["~standard"].validate(body);
|
|
61
|
+
if (result.issues) {
|
|
62
|
+
throw new MalformedResponseError(response, result.issues);
|
|
63
|
+
}
|
|
64
|
+
return handler.transform(result.value);
|
|
65
|
+
}
|
|
66
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
67
|
+
0 && (module.exports = {
|
|
68
|
+
ApiError,
|
|
69
|
+
MalformedResponseError,
|
|
70
|
+
UnrecognizedStatusError,
|
|
71
|
+
apiFetch
|
|
72
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var ApiError = class extends Error {
|
|
3
|
+
constructor(message, response, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.response = response;
|
|
6
|
+
this.name = this.constructor.name;
|
|
7
|
+
}
|
|
8
|
+
get status() {
|
|
9
|
+
return this.response?.status;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var UnrecognizedStatusError = class extends ApiError {
|
|
13
|
+
constructor(response) {
|
|
14
|
+
super(`Unrecognized status code: ${response.status}`, response);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var MalformedResponseError = class extends ApiError {
|
|
18
|
+
constructor(response, issues) {
|
|
19
|
+
super(`Malformed response body for status: ${response.status}`, response);
|
|
20
|
+
this.issues = issues;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
async function apiFetch(url, handlers, options = {}) {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
...options,
|
|
26
|
+
headers: { "Content-Type": "application/json", ...options.headers }
|
|
27
|
+
});
|
|
28
|
+
const handler = handlers[response.status];
|
|
29
|
+
if (!handler) {
|
|
30
|
+
throw new UnrecognizedStatusError(response);
|
|
31
|
+
}
|
|
32
|
+
const body = await response.json();
|
|
33
|
+
const result = await handler.schema["~standard"].validate(body);
|
|
34
|
+
if (result.issues) {
|
|
35
|
+
throw new MalformedResponseError(response, result.issues);
|
|
36
|
+
}
|
|
37
|
+
return handler.transform(result.value);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
ApiError,
|
|
41
|
+
MalformedResponseError,
|
|
42
|
+
UnrecognizedStatusError,
|
|
43
|
+
apiFetch
|
|
44
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@viliaapro/apifetch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A typed fetch wrapper with Standard Schema validation and status-code dispatch",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
19
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@standard-schema/spec": "^1.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@standard-schema/spec": "^1.0.0",
|
|
26
|
+
"tsup": "^8.0.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"fetch",
|
|
31
|
+
"typescript",
|
|
32
|
+
"standard-schema",
|
|
33
|
+
"http",
|
|
34
|
+
"api",
|
|
35
|
+
"rest"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|