Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tests, linting, and more #10

Merged
merged 12 commits into from
May 5, 2024
12 changes: 8 additions & 4 deletions .github/workflows/main.yml → .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Unit Tests
name: Build

on:
push:
Expand All @@ -7,13 +7,17 @@ on:
branches: [main]

jobs:
test:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 21
- run: npm ci
- run: npm test

- name: Install Dependencies
run: npm ci

- name: Build Package
run: npm run build
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 21

- name: Install Depedencies
run: npm ci

- name: Run Linter
run: npm run lint

- name: Run Formatter
run: npm run format:check

- name: Run Tests
run: npm run test:coverage
5 changes: 1 addition & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,8 @@
/coverage

# production
/dist
/dist**

# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
stable
21.6.2
5 changes: 5 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": true
}
12 changes: 0 additions & 12 deletions .vscode/settings.json

This file was deleted.

76 changes: 44 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,109 @@
[![Unit Tests](https://github.com/duncangrubbs/ozzy/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/duncangrubbs/ozzy/actions/workflows/main.yml)
[![Tests](https://github.com/duncangrubbs/ozzy/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/duncangrubbs/ozzy/actions/workflows/main.yml)

# 🦘 ozzy
# 🦘 ozzzy

> A small, easy to compose, API interface
> A small, easy to compose, HTTP API interface

Ozzy is a interface for interacting with APIs from a Javascript/Typescript client. It is essentially a stripped down version of axios with the inclusion of a middleware feature similar to [Express](https://expressjs.com/) middleware and [axios interceptors](https://axios-http.com/docs/interceptors). It was originally inspired by this [blog post](https://duncangrubbs.com/blog/oct012020) but has since been updated after more learning on my end. In fact, I recently wrote a new blog post about one of my favorite [uses of middleware](https://duncangrubbs.com/blog/jan122022).
Ozzzy is a interface for interacting with HTTP APIs from Javascript clients. It is essentially a stripped down version of [axios](https://axios-http.com) with the inclusion of a middleware feature similar to [Express](https://expressjs.com/) middleware and [axios interceptors](https://axios-http.com/docs/interceptors).

## 🤝 Design Principles

1. Lightweight
2. Easy to Use
1. Simple
2. Configurable
3. Zero Dependencies
4. Framework Agnostic

## 🔨 Docs

### REST Methods

Ozzy is similar to [axios](https://axios-http.com/docs/intro) in that is provides a core set of functions for making HTTP requests. You start by constructing an `Api` for a specific backend service that your client needs to interact with.
Ozzzy is similar to [axios](https://axios-http.com/docs/intro) in that is provides a core set of functions for making HTTP requests. You start by constructing an `HttpApi` for a specific backend service that your client needs to interact with. For example

```typescript
const userService = new Api(baseUrl, authService, ...middleware);
import { HttpApi } from 'ozzzy'

const userService = new HttpApi('/api/v1/users')
```

You provide the constructor with a `baseUrl`, an authentication service, and an optional collection of middleware functions. If you are writing Typescript, you can also type the API response that will be returned by all API requests. This type can be overridden at the request level though. This service now has a similar API to axios ... for example you can do something like
All the arguments to the constructor are optional, but you can provide a base URL if HTTP requests made with this service have one in common. You can also optionally pass a AuthProvider implementation, a set of shared headers, and a set of middleware functions.

This service now has a similar API to axios. For example to make a `GET` request

```typescript
import { handleErrors } from 'ozzzy'

try {
const userResponse = await userService.get<ResponseType>("/region/europe");
const userResponse = await userService.get<ResponseType>(
'/region/europe',
handleErrors,
)
} catch (error) {
console.error(error);
console.error(error)
}
```

Under the hood this builds the request headers and options, sends the fetch request, applies all of your middleware functions and returns you the final result. Ozzy support all common REST methods
Under the hood this builds the request headers and options, sends the fetch request, applies all of your middleware functions to the response and returns you the final result. Ozzzy supports `GET`, `PUT`, `POST`, and `DELETE`. For REST requests that support a body payload, you can pass any JSON serializable object as part of the request. For example

```typescript
Api.get<T>(url: string, ...middleware: any): Promise<T>;
Api.put<T>(url: string, payload: any, ...middleware: any): Promise<T>;
Api.post<T>(url: string, payload: any, ...middleware: any): Promise<T>;
Api.delete<T>(url: string, payload: any, ...middleware: any): Promise<T>;
const newUser = await userService.post<User>('/users/new', { name: 'Jane' })
```

### Auth

Ozzy supports basic auth out of the box. You can configure your auth at the service level by injecting the class into the `Api` constructor. To setup auth for the service, you build an `Auth` object.
To send an authorization HTTP header, you could simply build the header and pass it to the `HttpApi` constructor. That being said, it's recommended to either use the `OzzzyAuth` provider, or write your own provider that implements the `AuthProvider` interface. This way, different instances of `Api` can share the same `AuthProvider` instance. If you use `OzzzyAuth`, it supports a few basic auth schemes out of the box like `Bearer` and `Basic`. To use an `AuthProvider` instance, pass it to the constructor like so

```typescript
const auth = new Auth(AuthTypes.Bearer, userToken, 'Authorization')
import { HttpApi, OzzzyAuth, AuthTypes } from 'ozzzy'

const auth = new OzzzyAuth(AuthTypes.Bearer, userToken, 'Authorization')

const service = new Api(url, auth...)
const service = new HttpApi('/api/v1', auth)
```

This code configures the service to send the provided token in the `Authorization` header. The `AuthType` determines the format of this header. In the case the header would look like
This code configures the service to send the provided token in the `Authorization` header. The `AuthType` determines the format of this header. In this case the header would look like

```
Authorization: Bearer YOUR_TOKEN
```

### Middleware

At the core of Ozzy is the concept of middleware. This can be applied at the service level or the request level. Middleware intercepts the `Response` object, does something to it, and then passes it to the next middleware. It is important to keep in mind that order matters.
At the core of Ozzzy is the concept of middleware. This can be applied at the service level or the request level. Middleware intercepts the `Response` object, does something to it, and then passes it to the next middleware. It is important to keep in mind that order matters.

```typescript
const middlewareOne = (data, next) => {
const middlewareOne = async (data) => {
// do something to the response data
// of all requests made with this service
return next(data);
};
return data
}

const myService = new Api(baseUrl, new Auth(), middlewareOne);
const myService = new HttpApi('/api/v1', undefined, [], middlewareOne)

const middlewareTwo = (data, next) => {
const middlewareTwo = async (data) => {
// do something to the response data that is
// specific to this request
return next(data);
};
return data
}

const data = await myService.get("/api/foo/bar", middlewareTwo);
const data = await myService.get('/foo/bar', middlewareTwo)
```

For those of you who have written a lot of Javascript, you are probably familiar with writing something like this

```javascript
fetch("some url", headers, ...options)
fetch('https://some-url', headers, ...options)
// check the response status code
.then((response) => checkStatus(response))
// parse as json
.then((response) => response.json());
.then((response) => response.json())
// finally return the response
```

With Ozzy, you can write a middleware function once, and then apply it at the service level or request level. Out of convenience, ozzy comes with a few middlewares out of the box. Of course it is your choice if you want to apply these middlewares, but they are already written so that you do not have write them yourself. These include a middleware to parse the response as JSON, a basic logger, and a middleware to check the status code of the response and reject the promise if the `Response.ok` field is `false`.
With Ozzzy, you can write a middleware function once, and then apply it at the service level or request level. Out of convenience, Ozzzy comes with a few middlewares out of the box. Of course it is your choice if you want to apply these middlewares, but they are already written so that you do not have write them yourself. These include a middleware to parse the response as JSON, a basic logger, and a middleware to check the status code of the response and reject the promise if the `Response.ok` field is `false`.

## Testing Locally

If you are interesting in running the example file locally, you can simply run `npm i` and then `npm run example`. This example hit the sample JSON API and applies a variety of middlewares.

## 🙌 Contributing

Expand Down
6 changes: 6 additions & 0 deletions babel.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}
10 changes: 10 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";


export default [
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
Loading