Skip to content

Commit f04db72

Browse files
Merge pull request #1 from d1g1tinc/build-and-test
Readme, license, build, linting, tests
2 parents b4d5bcc + 361bbf2 commit f04db72

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2564
-365
lines changed

.eslintrc.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"root": true,
3+
"env": {
4+
"browser": true,
5+
"es6": true,
6+
"jest": true
7+
},
8+
"overrides": [
9+
{
10+
"files": ["**/*.tsx", "**/*.js"]
11+
}
12+
],
13+
"ignorePatterns": ["dist/**/*"],
14+
"extends": [
15+
"plugin:@typescript-eslint/eslint-recommended",
16+
"plugin:@typescript-eslint/recommended",
17+
"eslint:recommended",
18+
"plugin:react/recommended",
19+
"plugin:prettier/recommended",
20+
"prettier/react",
21+
"prettier/@typescript-eslint"
22+
],
23+
"parser": "@typescript-eslint/parser",
24+
"parserOptions": {
25+
"ecmaFeatures": {
26+
"jsx": true
27+
},
28+
"ecmaVersion": 2018,
29+
"sourceType": "module"
30+
},
31+
32+
"settings": {
33+
"react": {
34+
"version": "detect"
35+
}
36+
},
37+
38+
"plugins": ["@typescript-eslint", "react", "prettier", "react-hooks"],
39+
"rules": {
40+
"prettier/prettier": 1,
41+
"@typescript-eslint/explicit-function-return-type": 0,
42+
"@typescript-eslint/interface-name-prefix": [1, {"prefixWithI": "never"}],
43+
"@typescript-eslint/no-empty-interface": 0,
44+
"@typescript-eslint/no-explicit-any": 0,
45+
"@typescript-eslint/no-unused-vars": 0,
46+
"@typescript-eslint/no-use-before-define": 0,
47+
"react/prop-types": 0,
48+
"no-unused-vars": 0,
49+
"no-undef": 0,
50+
"no-console": [
51+
"error",
52+
{"allow": ["table", "warn", "error", "info", "group", "groupEnd"]}
53+
]
54+
}
55+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
node_modules
2+
dist
3+
coverage
4+
.DS_Store

.prettierrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"jsxSingleQuote": true,
5+
"trailingComma": "none",
6+
"bracketSpacing": false,
7+
"jsxBracketSameLine": false,
8+
"arrowParens": "always"
9+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 D1G1T INC
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 267 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,268 @@
1-
# Api
1+
# `restii`
22

3-
This repository contains API functionality extracted from the d1g1t UI's `api` layer.
3+
Energize your REST API 🌿 with React hooks and a centralized cache.
4+
5+
![npm bundle size](https://img.shields.io/bundlephobia/minzip/restii) ![npm type definitions](https://img.shields.io/npm/types/restii) ![GitHub stars](https://img.shields.io/github/stars/d1g1tinc/restii?style=social)
6+
7+
## Features
8+
9+
- 🚀 A set of React hooks for querying HTTP API data
10+
- 💾 Turnkey API response caching
11+
- 🖇 Parallel request de-duplication
12+
- 📡 Support for API requests and cache queries outside of React components (in sagas, thunks, etc.)
13+
- 📥 Parses response bodies of any data type (`'json'`, `'blob'`, `'text'`)
14+
- 💡 Designed with full Typescript support
15+
16+
## Motivation
17+
18+
At [d1g1t](https://github.com/d1g1tinc), we make over 400 REST API calls in our enterprise investment advisor platform. Over time, as we started using React hooks and wanted to introduce caching optimizations, it was clear that we needed to overhaul our internal REST API fetching library to use patterns that scale with our app.
19+
20+
`restii` synthesizes patterns from other libraries, such as [`apollo-client`](https://www.apollographql.com/docs/react/), [`swp`](https://github.com/zeit/swr), and [`react-query`](https://github.com/tannerlinsley/react-query). The primary difference is that it's specifically designed for making HTTP calls to your API. It allows you to request API data with URL paths, query parameters, request bodies, and HTTP headers. The caching layer will deterministically map these HTTP request parameters to response bodies, allowing the user to easily query their API and defer caching logic to `restii`.
21+
22+
Since it works well for d1g1t's purposes, we decided to open-source the library to help others who are building a REST API-consuming React application.
23+
24+
## Basic Usage
25+
26+
Query your API (ex. fetching a user's profile):
27+
28+
```jsx
29+
import React from 'react'
30+
import {useApiQuery} from 'restii'
31+
32+
const MyComponent = (props) => {
33+
const [userQuery] = useApiQuery({url: `/users/${props.userId}`})
34+
35+
if (userQuery.error) {
36+
// display error
37+
}
38+
39+
if (userQuery.loading) {
40+
// display loading state
41+
}
42+
43+
return <h1>{userQuery.data.firstName}</h1>
44+
}
45+
```
46+
47+
As you start adding more API requests, we strongly recommend organizing your request definitions into centralized "endpoint" classes, grouped by domain/resource.
48+
49+
Continuing our example, we'll create a `UserEndpoints` class to define endpoints under the `'/users'` base path. We'll subclass `restii#HttpEndpoints`, which gives us static HTTP helper methods:
50+
51+
```jsx
52+
import {HttpEndpoints} from 'restii'
53+
54+
export class UserEndpoints extends HttpEndpoints {
55+
static basePath = '/users'
56+
57+
static list(query) {
58+
return super._get('', {query})
59+
// {method: 'GET', url: '/users?serializedQuery'}
60+
}
61+
62+
static create(body) {
63+
return super._post('', {body})
64+
// {method: 'POST', url: '/users'}
65+
}
66+
67+
static findById(id) {
68+
return super._get(`/${id}`)
69+
// {method: 'GET', url: `/users/${id}`}
70+
}
71+
72+
static update(id, body) {
73+
return super._put(`/${id}`, {body})
74+
// {method: 'PUT', url: `/users/${id}`, body}
75+
}
76+
77+
static partialUpdate(id, body) {
78+
return super._patch(`/${id}`, {body})
79+
// {method: 'PATCH', url: `/users/${id}`, body}
80+
}
81+
82+
static destroy(id) {
83+
return super._delete(`/${id}`)
84+
// {method: 'DELETE', url: `/users/${id}`}
85+
}
86+
87+
// ad-hoc, custom request:
88+
static requestPasswordReset(id, resetToken, body) {
89+
return super._post(`/users/${id}`, {
90+
body,
91+
headers: {'x-reset-token': resetToken}
92+
})
93+
// {method: 'POST', url: `/users/${id}`, headers: {'x-reset-token': resetToken}, body}
94+
}
95+
}
96+
```
97+
98+
If your endpoints follow common REST-ful conventions, you can subclass `restii#RestEndpoints` (which subclasses `restii#HttpEndpoints`) to reduce REST boilerplate:
99+
100+
```jsx
101+
import {RestEndpoints} from 'restii'
102+
103+
export class UserEndpoints extends RestEndpoints {
104+
static basePath = '/users'
105+
106+
static list(query) {
107+
return super._list(query)
108+
}
109+
110+
static create(body) {
111+
return super._create(body)
112+
}
113+
114+
static findById(id) {
115+
return super._findById(id)
116+
}
117+
118+
static update(id, body) {
119+
return super._update(id, body)
120+
}
121+
122+
static partialUpdate(id, body) {
123+
return super._partialUpdate(id, body)
124+
}
125+
126+
static destroy(id) {
127+
return super._destroy(id)
128+
}
129+
130+
static requestPasswordReset(id, resetToken, body) {
131+
return super._post(`/users/${id}`, {
132+
body,
133+
headers: {'x-reset-token': resetToken}
134+
})
135+
}
136+
}
137+
```
138+
139+
Then you can use these endpoints to make queries:
140+
141+
```jsx
142+
import React from 'react'
143+
import {useApiQuery} from 'restii'
144+
145+
import {UserEndpoints} from 'my-app/endpoints'
146+
147+
const MyComponent = (props) => {
148+
const [usersQuery] = useApiQuery(UserEndpoints.list({limit: 10}))
149+
const [userQuery] = useApiQuery(UserEndpoints.findById(props.userId))
150+
// ... etc
151+
}
152+
```
153+
154+
To make one-off requests (ie. form submissions, deletions, etc), you can use the `Api` client instance directly:
155+
156+
```jsx
157+
import React, {useState} from 'react'
158+
import {useApi} from 'restii'
159+
160+
import {UserEndpoints} from 'my-app/endpoints'
161+
162+
const DeleteUser = (props) => {
163+
const api = useApi()
164+
const [deleting, setDeleting] = useState(false)
165+
166+
const handleDelete = async () => {
167+
setDeleting(true)
168+
169+
try {
170+
await api.request(UserEndpoints.destroy(props.userId))
171+
// navigate to a different page, etc.
172+
} catch (error) {
173+
// handle error
174+
} finally {
175+
setDeleting(false)
176+
}
177+
}
178+
179+
return (
180+
<>
181+
<button type='button' onClick={handleDeleteUser} disabled={deleting}>
182+
Delete User
183+
</button>
184+
</>
185+
)
186+
}
187+
```
188+
189+
## Installation & Setup
190+
191+
Install the package as a dependency:
192+
193+
```bash
194+
npm install --save restii
195+
196+
# or
197+
198+
yarn add restii
199+
```
200+
201+
Create an `Api` instance and provide it to your app:
202+
203+
```jsx
204+
import {Api, ApiProvider} from 'restii'
205+
206+
const api = new Api({
207+
// ↓ prefixes all request urls
208+
baseUrl: 'http://your-api.com'
209+
})
210+
211+
const App = () => (
212+
<ApiProvider api={api}>{/* render your app here */}</ApiProvider>
213+
)
214+
```
215+
216+
You can now start defining endpoints and making requests in your app.
217+
218+
## Guide
219+
220+
### Caching
221+
222+
#### How cache data is keyed
223+
224+
TODO explain how requests are deterministically keyed for caching (without request body)
225+
226+
#### Using the cache when requesting data
227+
228+
TODO explain how to use `fetchPolicy`
229+
230+
#### Writing to the cache directly
231+
232+
TODO
233+
234+
### Dependent queries
235+
236+
TODO
237+
238+
### Re-fetching a query
239+
240+
TODO
241+
242+
### Custom response body parsing
243+
244+
TODO
245+
246+
### Custom query string serialization
247+
248+
TODO
249+
250+
### Setting default headers (ie. an auth token) for all requests
251+
252+
TODO
253+
254+
### Typescript
255+
256+
TODO
257+
258+
## Comparison to similar libraries
259+
260+
TODO: add comparisons to `react-query`/`swr`, `rest-hooks`, `apollo-graphql` (with `apollo-link-rest`)
261+
262+
## Usage with Redux
263+
264+
TODO
265+
266+
## API
267+
268+
TODO

jest.config.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
globals: {
4+
'ts-jest': {
5+
tsConfig: 'tsconfig.test.json',
6+
diagnostics: {
7+
warnOnly: true
8+
}
9+
}
10+
},
11+
12+
testEnvironment: 'jsdom',
13+
setupFiles: ['./setup-jest.js'],
14+
setupFilesAfterEnv: ['./setup-jest-after-env.tsx'],
15+
collectCoverageFrom: ['./src/**/*.tsx'],
16+
coverageThreshold: {
17+
global: {
18+
branches: 100,
19+
functions: 100,
20+
lines: 100,
21+
statements: 100
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)