npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

oolio

v0.2.9

Published

A universal API client library

Readme

oolio

범용 API 클라이언트 라이브러리입니다. RESTful API를 쉽게 호출할 수 있도록 도와줍니다.

설치

npm install oolio

특징

  • 라우트 기반의 API 클라이언트
  • 자동 인증 토큰 처리 (Bearer)
  • 경로 파라미터 지원 (/user/{userId})
  • 본문 자동 직렬화 — 일반 POST/PUT/DELETE는 application/json, payload 값에 File/Blob, React Native 파일 객체({ uri, type, name? }), Node.js Buffer가 있으면 multipart/form-data로 자동 전환
  • 통일된 에러 처리 형식
  • 브라우저 / Node.js / React Native 환경 모두 지원
  • TypeScript 제네릭으로 요청/응답 타입 정의 가능
  • fetchOptions로 fetch init 옵션 주입 (credentials, mode, cache, signal 등) — 쿠키 인증 지원

사용법

JavaScript

import oolio from "oolio";

const routes = {
  auth: {
    login: {
      method: "post",
      path: "/auth/login",
      payload: ["email", "password"],
    },
  },
  user: {
    getProfile: {
      method: "get",
      path: "/user/profile",
    },
    getUserById: {
      method: "get",
      path: "/user/{userId}",
    },
    updateUserById: {
      method: "put",
      path: "/user/{userId}",
      payload: ["name", "email"],
    },
    uploadAvatar: {
      method: "post",
      path: "/user/avatar",
      payload: ["userId", "avatar"],
    },
  },
};

const api = oolio({
  routes,
  getAuthorizeToken: () => localStorage.getItem("token"),
  baseUrl: "https://api.example.com",
});

// 일반 요청
const response = await api.auth.login({
  email: "[email protected]",
  password: "1234",
});

// 경로 파라미터
const user = await api.user.getUserById({ userId: "123" });

// 경로 파라미터 + payload
await api.user.updateUserById(
  { userId: "123" },
  { name: "John", email: "[email protected]" },
);

// 파일 업로드
await api.user.uploadAvatar({ userId: "123", avatar: fileInput.files[0] });

TypeScript

IO<TPayload, TResponse> 제네릭으로 요청/응답 타입을 정의할 수 있습니다.

import oolio from "oolio";
import type { IO } from "oolio";

const routes = {
  auth: {
    login: {
      method: "post",
      path: "/auth/login",
      payload: ["email", "password"],
    } as IO<{ email: string; password: string }, { token: string }>,
  },
  user: {
    getProfile: {
      method: "get",
      path: "/user/profile",
    } as IO<void, { name: string; avatar: string }>,

    getUserById: {
      method: "get",
      path: "/user/{userId}",
    } as IO<void, { id: string; name: string }>,

    updateUserById: {
      method: "put",
      path: "/user/{userId}",
      payload: ["name", "email"],
    } as IO<{ name: string; email: string }, { success: boolean }>,

    uploadAvatar: {
      method: "post",
      path: "/user/avatar",
      payload: ["avatar"],
    } as IO<{ avatar: File }, { url: string }>,
  },
};

const api = oolio({
  routes,
  getAuthorizeToken: () => localStorage.getItem("token"),
  baseUrl: "https://api.example.com",
});

// 타입 자동 추론
const { token } = await api.auth.login({
  email: "[email protected]",
  password: "1234",
});
const { name } = await api.user.getProfile();
const { id } = await api.user.getUserById({ userId: "123" });

// 경로 파라미터 + payload
await api.user.updateUserById(
  { userId: "123" },
  { name: "John", email: "[email protected]" },
);

IO 타입 규칙: IO<TPayload, TResponse>TPayload는 body(POST/PUT 등) 또는 query string(GET) 필드 타입만 기술합니다. path의 {param} 패턴으로 선언한 path params는 IO 타입에 포함하지 않습니다. path params는 항상 Record<string, string>으로 처리되며, 라이브러리가 런타임에 자동으로 분리합니다.

// ❌ 잘못된 예: path param을 TPayload에 포함
updateUserById: {
  method: "put",
  path: "/user/{userId}",
  payload: ["name", "email"],
} as IO<{ userId: string; name: string; email: string }, { success: boolean }>,
//         ^^^^^^^^ path param — IO에 넣으면 안 됨

// ✅ 올바른 예: payload 필드(body에 담길 것들)만 기술
updateUserById: {
  method: "put",
  path: "/user/{userId}",
  payload: ["name", "email"],
} as IO<{ name: string; email: string }, { success: boolean }>,

// ✅ path params만 있고 payload 없는 경우: TPayload는 void
getUserById: {
  method: "get",
  path: "/user/{userId}",
} as IO<void, { id: string; name: string }>,

라우트 옵션

| 옵션 | 필수 | 설명 | | --------------- | ---- | ---------------------------------------------------------------------- | | method | O | HTTP 메소드 (get, post, put, delete, patch 등). 대소문자 무관 — 와이어에는 대문자로 정규화되어 전송 | | path | O | API 엔드포인트 경로. 경로 파라미터는 {param} 형식 | | payload | - | 요청에 포함될 데이터 필드 목록 (파일 필드도 여기에 함께 명시) | | authorization | - | false 또는 "guest" 설정 시 토큰 미첨부 (기본값: true) | | baseUrl | - | 라우트별 baseUrl 오버라이드 |

파일 업로드는 별도 옵션 없이 payload에 키만 명시하면 됩니다. 호출 시 해당 값이 File/Blob, React Native 파일 객체({ uri: string, type: string, name?: string }), Node.js Buffer 중 하나이면 자동으로 multipart/form-data로 전송됩니다.

메서드 대소문자: method는 대소문자를 가리지 않습니다. GET 판정("get"/"GET" 모두 인식)과 본문 직렬화 분기는 내부적으로 대소문자 무관하게 처리되고, 실제 fetch 호출에 넘기는 와이어 메서드는 대문자로 정규화됩니다("patch"PATCH).

특히 PATCH가 중요합니다 — fetch 스펙은 PATCH를 자동 대문자화 대상에서 제외하므로 fetch(url, { method: "patch" })는 소문자 patch가 그대로 와이어에 나가고, 일부 서버(예: Next 16 Node HTTP)는 이를 malformed로 간주해 빈 400으로 끊습니다. oolio는 이를 라이브러리 차원에서 방지하므로 소비측 routes는 메서드를 소문자로 둬도 안전합니다.

클라이언트 옵션 (option)

oolio({ ..., option })에 전달하는 클라이언트 단위 설정.

| 옵션 | 기본값 | 설명 | | -------------- | ------ | ------------------------------------------------------------------------------ | | logger | false | true 설정 시 모든 요청·응답·에러를 console에 출력 | | loggerPretty | false | true 설정 시 객체를 JSON.stringify로 전체 depth 출력 (logger: true일 때만 적용) |

로그 활성화

const api = oolio({
  routes,
  getAuthorizeToken: () => localStorage.getItem("token"),
  baseUrl: "https://api.example.com",
  option: { logger: true },
});

호출마다 6자 임시 ID가 발급되어 모든 로그 라인 prefix([oolio]:{id})에 포함되므로, 동시 호출 시에도 같은 요청의 로그를 ID로 묶어서 추적할 수 있습니다.

[oolio]:k3p9af → { method: 'post', path: '/auth/login', ... } { pathParams: {}, data: {...}, headers: { Authorization: 'Bearer e...XYZ12345' } }
[oolio]:k3p9af → POST https://api.example.com/auth/login
[oolio]:k3p9af ← POST https://api.example.com/auth/login (245ms) { token: '...' }

Authorization 헤더는 토큰 노출을 줄이기 위해 부분 마스킹됩니다(앞/뒤 일부만 표시). 그 외 body·data는 마스킹 없이 그대로 출력되므로 운영 환경에서는 활성화하지 않는 것을 권장합니다.

중첩 객체 전체 출력 (loggerPretty)

기본적으로 Node.js의 console.log는 객체를 depth 2까지만 출력해 중첩된 값이 [Object]로 잘립니다. loggerPretty: true를 함께 설정하면 객체를 JSON.stringify로 포매팅해 전체 내용을 확인할 수 있습니다.

const api = oolio({
  routes,
  getAuthorizeToken: () => null,
  baseUrl: "https://api.example.com",
  option: { logger: true, loggerPretty: true },
});
// logger: true 만 설정한 경우
[oolio]:k3p9af ← GET https://api.example.com/items (120ms) { data: { items: [Array], total: 1 } }

// loggerPretty: true 추가 시
[oolio]:k3p9af ← GET https://api.example.com/items (120ms) {
  "data": {
    "items": [
      { "id": 1, "name": "example" }
    ],
    "total": 1
  }
}

본문 직렬화 규칙

axios의 동작과 유사하게, 메소드와 호출 시 데이터에 따라 자동으로 본문 형식이 결정됩니다.

| 조건 | Content-Type | 본문 | | --------------------------------------------------------------------------- | ------------------------------------- | ----------------------------- | | method: "get" (대소문자 무관) | (없음) | URL query string | | payload 값 중 File/Blob 인스턴스 존재 | multipart/form-data (자동) | FormData (자동 변환) | | payload 값 중 RN 파일 객체 ({ uri, type, name? }) 존재 | multipart/form-data (자동) | FormData (자동 변환) | | payload 값 중 Node.js Buffer 존재 | multipart/form-data (자동) | FormData (자동 변환) | | 호출 시 dataFormData 인스턴스 직접 전달 | multipart/form-data (자동) | 전달한 FormData 그대로 | | 그 외 POST/PUT/DELETE | application/json | JSON.stringify(payload) |

  • 사용자가 headers["Content-Type"]을 직접 지정한 경우 항상 그 값을 우선합니다 — 자동 분기로 multipart가 되는 경우에도 delete하지 않고 사용자가 지정한 값을 그대로 보냅니다 (단, multipart/form-data로 임의 지정 시 boundary는 사용자 책임).
  • payload에 명시되지 않은 키는 자동 감지 대상에서 제외되어 잘려나갑니다 (협업 누락 방지를 위한 의도적 동작).

fetch 옵션 (fetchOptions)

oolio({ ..., fetchOptions })에 전달하면 모든 요청의 fetch(url, init) 호출에 병합되는 init 옵션입니다. credentials, mode, cache, signal, keepalive 등 표준 RequestInit 필드를 지정할 수 있습니다.

쿠키 기반 인증(특히 cross-origin)에는 credentials: "include"가 필요합니다. 지정하지 않으면 fetch 기본값(same-origin)이 적용됩니다.

const api = oolio({
  routes,
  getAuthorizeToken: () => localStorage.getItem("token"),
  baseUrl: "https://api.example.com",
  fetchOptions: { credentials: "include" }, // 모든 요청에 쿠키 동봉
});
  • 호출별 오버라이드: 마지막 인자로 { fetchOptions }를 전달하면 해당 요청에만 적용됩니다. 클라이언트 레벨 fetchOptions와 얕은 병합되며 호출별 값이 우선합니다.
  • 우선순위: oolio가 관리하는 method/body/headers는 항상 최종 우선합니다. fetchOptions.headers를 지정해도 직렬화 단계에서 만든 헤더(Authorization, Content-Type 등)가 같은 키를 덮어씁니다.
  • 인터셉터 노출: 병합된 값은 RequestConfig.fetchOptions로 들어가 request 인터셉터에서 읽고 변형할 수 있습니다.
// 이 호출만 same-origin으로 (클라이언트 기본 include를 덮어씀)
await api.user.getProfile({ fetchOptions: { credentials: "same-origin" } });

// pathParams + data + fetchOptions
await api.user.updateUserById(
  { userId: "123" },
  { name: "John", email: "[email protected]" },
  { fetchOptions: { cache: "no-store" } },
);

// headers와 함께 사용
await api.user.getProfile({
  headers: { "X-Trace-Id": "abc" },
  fetchOptions: { credentials: "include" },
});

인터셉터

oolio({ ..., interceptors })에 전달하는 훅. 한 번 등록하면 모든 요청에 자동 적용됩니다.

| 훅 | 시점 | 시그니처 | | ---------------- | ----------------------------- | ------------------------------------------------------------------------------------------ | | request | fetch 직전 (직렬화 완료 후) | (config: RequestConfig) => RequestConfig \| Promise<RequestConfig> | | response | 성공 응답 파싱 후 | (data: any, config: RequestConfig) => any \| Promise<any> | | responseError | 에러 최종 처리 | (error: OolioError, config: RequestConfig) => any \| Promise<any> | | retry | 에러 발생 시 재시도 여부 결정 | (error: OolioError, config: RequestConfig, attempt: number) => boolean \| Promise<boolean> |

  • attempt: 지금까지 실패한 횟수. 첫 실패 후 호출 시 attempt=1.
  • retrytrue를 반환하면 동일 config로 재시도합니다. responseError보다 먼저 실행되며, retry 포기 후 responseError로 넘어갑니다.
  • responseError가 값을 반환하면 해당 값이 호출자에게 전달됩니다 (throw 없음). 직접 throw하면 호출자까지 전파됩니다.
const api = oolio({
  routes,
  getAuthorizeToken: () => localStorage.getItem("token"),
  baseUrl: "https://api.example.com",
  interceptors: {
    // 모든 요청에 트레이스 ID 헤더 추가
    request: (config) => {
      config.headers["X-Trace-Id"] = crypto.randomUUID();
      return config;
    },
    // 응답 unwrap: { result: ... } 구조라면 result만 반환
    response: (data) => data.result ?? data,
    // 404는 null로 변환, 나머지는 그대로 throw
    responseError: async (err) => {
      if (err.status === 404) return null;
      throw err;
    },
    // 503 에러 최대 3회 지수 백오프 재시도
    retry: async (err, _config, attempt) => {
      if (err.status !== 503 || attempt >= 3) return false;
      await new Promise((r) => setTimeout(r, 2 ** attempt * 100));
      return true;
    },
  },
});

per-request 옵션

각 API 호출의 마지막 인자{ headers?: Record<string, string>; fetchOptions?: RequestInit } 오브젝트를 전달하면 해당 요청에만 적용됩니다. 글로벌 인터셉터로 처리하기 어려운 요청별 헤더나 fetch 옵션에 사용합니다.

// path params 없는 route — data 없이 headers만
api.user.getProfile({ headers: { "X-Request-Source": "mobile" } });

// path params 없는 route — data + headers
api.auth.login({ email: "[email protected]", password: "1234" }, { headers: { "X-Trace-Id": "abc" } });

// path params 있는 route — data 생략, pathParams + headers
api.user.getUserById({ userId: "123" }, { headers: { "X-Request-Source": "admin" } });

// path params 있는 route — pathParams + data + headers
api.user.updateUserById(
  { userId: "123" },
  { name: "John", email: "[email protected]" },
  { headers: { "X-Trace-Id": "abc" } },
);

headers 또는 fetchOptions 키를 가진 오브젝트를 마지막 인자로 넣으면 options로 인식합니다. data를 생략하고 싶다면 null 없이 바로 붙이면 됩니다.

// path params 있는 route — data 생략
// { headers } 키가 있으므로 options로 자동 인식
api.user.getUserById({ userId: "123" }, { headers: { "X-Custom": "test" } });

// fetchOptions만 단독으로 넘겨도 options로 인식
api.user.getProfile({ fetchOptions: { credentials: "include" } });

fetch 옵션 주입에 대한 자세한 내용은 fetch 옵션 (fetchOptions) 섹션을 참고하세요.

에러 처리

try {
  const response = await api.auth.login({
    email: "[email protected]",
    password: "1234",
  });
} catch (error) {
  console.error(error.status); // HTTP 상태 코드
  console.error(error.statusText); // 상태 텍스트
  console.error(error.data); // 서버 응답 데이터
}

변경 이력

0.2.9

버그 수정

  • 와이어 HTTP 메서드를 대문자로 정규화 — fetch 스펙이 PATCH를 자동 대문자화 대상에서 제외해 소문자 patch가 그대로 전송되던 문제 수정. 일부 서버(예: Next 16 Node HTTP)가 소문자 메서드를 malformed로 간주해 빈 400으로 끊던 함정을 라이브러리 차원에서 방지
  • GET 판정 및 본문 직렬화 분기를 대소문자 무관하게 변경 — method"GET"(대문자)로 선언해도 정상적으로 query string 직렬화

0.2.8

신규 기능

  • OolioConfig.fetchOptions 추가 — 모든 요청의 fetch 호출에 병합할 init 옵션(credentials, mode, cache, signal 등) 주입. 쿠키 기반 인증(특히 cross-origin)을 위한 credentials: "include" 지원
  • per-request options.fetchOptions 추가 — 호출별로 fetch 옵션 오버라이드 (클라이언트 레벨과 얕은 병합, 호출별 우선)
  • 마지막 인자에 fetchOptions 키만 있어도 per-request options로 자동 인식 (기존 headers 키와 동일하게 동작)
  • 병합된 fetch 옵션은 RequestConfig.fetchOptions로 노출되어 request 인터셉터에서 변형 가능. oolio가 관리하는 method/body/headers는 항상 최종 우선

0.2.7

버그 수정

  • React Native 환경에서 파일 업로드가 동작하지 않던 문제 수정 — RN 파일 객체({ uri, type, name? })를 binary로 감지해 multipart/form-data로 자동 전환
  • Node.js Buffer를 파일로 업로드할 수 없던 문제 수정 — Buffer 인스턴스를 binary로 감지해 multipart/form-data로 자동 전환

0.2.6

문서 개선

  • IO<TPayload, TResponse> 타입 규칙 명시 — TPayload는 body/query 필드 전용이며 path params는 포함하지 않는다는 설명과 올바른/잘못된 예시 추가

0.2.5

버그 수정

  • ApiClient 타입이 path params 있는 route에서 인자 2개를 받지 못하던 문제 수정 — (pathParams, data?) 시그니처를 오버로드로 추가해 TypeScript 에러 해소

0.2.4

신규 기능

  • OolioConfig.option.loggerPretty 추가 — logger: true일 때 중첩 객체를 JSON.stringify로 전체 depth 출력

0.2.3

Breaking changes

  • IO 옵션에서 files 필드 제거. 대신 payload 값 중 File/Blob 인스턴스가 있으면 자동으로 multipart/form-data로 전환됩니다. 기존에 files를 사용하던 경우 해당 키를 payload로 옮기기만 하면 됩니다.
  • POST/PUT/DELETE 기본 직렬화 방식이 multipart/form-dataapplication/json으로 변경되었습니다.

신규 기능

  • OolioConfig.interceptors 추가 — request / response / responseError / retry 4종 지원
  • OolioConfig.option.logger 추가 — 요청·응답·에러 콘솔 출력, 동시 요청 구분용 임시 ID 포함
  • per-request 옵션 — 마지막 인자로 { headers } 오브젝트 전달 시 해당 요청에만 헤더 적용
  • 사용자가 headers["Content-Type"]을 직접 지정한 경우 자동 감지보다 우선 적용
  • payload 값에 File/Blob이 있으면 별도 설정 없이 multipart/form-data로 자동 전환

라이센스

MIT