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

servios

v1.0.17

Published

servios is a lightweight foundational library that provides essential building blocks for modern applications, including core utilities, API abstractions, integration providers, caching mechanisms, and shared logic.It is designed to centralize common func

Readme

Servios

Production-ready HTTP client with automatic token refresh, decorators, and zero boilerplate

npm version License: MIT TypeScript

Servios is a lightweight, type-safe HTTP client that eliminates API boilerplate. Configure once, use everywhere with automatic token management and elegant decorator syntax.


✨ Why Servios?

  • 🎯 Zero boilerplate - Configure once globally, use in all services
  • 🔄 Auto token refresh - 401 errors? Handled automatically with request queuing
  • 🎭 Decorator magic - @Public() for unauthenticated endpoints
  • 🌐 Smart URLs - Auto-generates {baseURL}/{serviceName}/{version}/{endpoint}
  • 🔐 Flexible storage - Cookie, localStorage, or sessionStorage for tokens
  • TypeScript first - Full type safety with excellent IDE support
  • 🧪 Mock-ready - Built-in mock adapter for testing

📦 Installation

npm install servios
# or
yarn add servios

🚀 Quick Start

Step 1: Configure Once (Global Setup) (React)

For Next.js you do not need this

go to step 2b

Create a config file and set up Servios once for your entire app:

// src/config/api.ts
import { configureBaseService } from 'servios';

configureBaseService({
  baseURL: 'https://api.example.com',

  // Automatic token refresh on 401
  async refreshToken() {
    const response = await fetch('/auth/refresh', {
      credentials: 'include'
    });
    const data = await response.json();
    return { accessToken: data.accessToken };
  },

  // Logout handler (called when refresh fails)
  logout: () => {
    window.location.href = '/login';
  },
});

That's it! Now all services share this configuration.


Step 2a: Create Services (React Setup)

Extend ApiService to create your API services. Each service automatically inherits the global configuration.

// src/services/UserService.ts
import { ApiService } from 'servios';

class UserService extends ApiService {
  constructor() {
    super({
      serviceName: 'users',
      version: 'v2',  // default: 'v1'
    });
  }

  // Authenticated endpoint - token automatically added
  getMe() {
    return this.get({ endpoint: 'me' });
    // GET https://api.example.com/users/v2/me
  }

  updateProfile(data: any) {
    return this.patch({ endpoint: 'profile', data });
    // PATCH https://api.example.com/users/v2/profile
  }
}

export default new UserService();

Step 2b: Create Services (Next.js Setup)

Extend BaseService to create your API services. Each service automatically inherits the global configuration.

// src/services/api.ts
import { BaseService } from "servios";

export class JsonService extends BaseService {
    constructor() {
        super({
            baseURL: "https://jsonplaceholder.typicode.com/",
            isPublic: true,
            getAccessToken: () => "",
            setAccessToken: () => "",
            getRefreshToken: () => "",
            setRefreshToken: () => "",
        });
    }
}

// src/services/Posts.ts
import { JsonService } from './api';

export class PostService extends JsonService {
    getPost() {
        return this.api.get("/posts")
    }
}

// page.tsx
import { PostService } from "./services/userService";

const postService = new PostService();

async function Home() {
  const data = await postService.getPost();
  return <p>{data?.data?.[0]?.title}</p>;
}

export default Home;

Step 3: Use in Components

import userService from '@/services/UserService';

async function loadProfile() {
  try {
    const { user } = await userService.getMe();
    console.log(user);
  } catch (error) {
    console.error('Failed to load profile:', error);
  }
}

🎯 Core Concepts

configureBaseService - Global Configuration

Call once at app startup. All services inherit these settings.

configureBaseService({
  // Required
  baseURL: 'https://api.example.com',

  // Optional - Token refresh (highly recommended)
  async refreshToken() {
    const res = await fetch('/auth/refresh', { credentials: 'include' });
    return await res.json();
  },

  // Optional - Logout handler
  logout: () => router.push('/login'),

  // Optional - Token storage (default: cookie)
  tokenConfig: {
    storage: 'cookie',  // 'cookie' | 'localStorage' | 'sessionStorage'
  },

  // Optional - Retry on status codes
  retryOnStatusCodes: [401],  // default

  // Optional - Error transform
  transformError: (error) => ({
    message: error.response?.data?.message || 'Something went wrong',
  }),
});

ApiService vs BaseService

| Class | When to use | Configuration | |-------|------------|---------------| | ApiService | ✅ Use this for app services | Inherits global config + service-specific options | | BaseService | ⚠️ Rare cases only | Must provide ALL configuration manually |

Example: Using ApiService (Recommended)

// Global config already has baseURL, refreshToken, etc.
class UserService extends ApiService {
  constructor() {
    super({ serviceName: 'users' });  // Only service-specific config
  }
}

Example: Using BaseService (Only if needed)

// Use when you need a completely different configuration
class ExternalApiService extends BaseService {
  constructor() {
    super({
      baseURL: 'https://external-api.com',  // Different API
      serviceName: 'external',
      // Must provide ALL configuration here
      getAccessToken: () => localStorage.getItem('externalToken'),
      // ... all other options
    });
  }
}

🔓 Public Endpoints (No Authentication)

Servios provides three ways to mark endpoints as public (no token sent).

1. Class-Level Decorator ⭐ Best for fully public services

Mark the entire service as public - all methods skip authentication:

import { ApiService, Public } from 'servios';

@Public()
class PublicOrgService extends ApiService {
  constructor() {
    super({ serviceName: 'org' });
  }

  // All methods are automatically public
  getOrgs() {
    return this.get({ endpoint: 'list' });
  }

  getOrgById(id: string) {
    return this.get({ endpoint: `${id}` });
  }
}

export default new PublicOrgService();

2. Method-Level Decorator ⭐ Best for mixed services

Some methods public, others authenticated:

import { ApiService, Public } from 'servios';

class OrgService extends ApiService {
  constructor() {
    super({ serviceName: 'org' });
  }

  // Public - no token sent
  @Public()
  getPublicOrgs() {
    return this.get({ endpoint: 'public/list' });
  }

  @Public()
  getPublicOrgById(id: string) {
    return this.get({ endpoint: `public/${id}` });
  }

  // Authenticated - token automatically sent
  createOrg(data: any) {
    return this.post({ endpoint: 'create', data });
  }

  updateOrg(id: string, data: any) {
    return this.put({ endpoint: `${id}`, data });
  }
}

export default new OrgService();

Note: To use decorators, enable experimentalDecorators in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

3. Per-Request Flag (Alternative)

class OrgService extends ApiService {
  getPublicOrgs() {
    return this.get({
      endpoint: 'list',
      isPublic: true,  // Skip auth for this request
    });
  }
}

4. Service-Level Flag (Constructor)

class PublicApiService extends ApiService {
  constructor() {
    super({
      serviceName: 'public',
      isPublic: true,  // All requests skip authentication
    });
  }
}

🔒 Private Decorator (Override Public)

The @Private() decorator allows you to mark specific methods as authenticated even when the entire class is marked as @Public().

Use Case: Mostly Public Service with Few Protected Methods

import { ApiService, Public, Private } from 'servios';

@Public()  // Class is public by default
class PostsService extends ApiService {
  constructor() {
    super({ serviceName: 'posts' });
  }

  // ✅ Public - inherits from class decorator
  getAllPosts() {
    return this.get({ endpoint: 'list' });
  }

  // ✅ Public - inherits from class decorator
  getPostById(id: string) {
    return this.get({ endpoint: `${id}` });
  }

  // 🔐 Authenticated - @Private overrides class-level @Public
  @Private()
  getMyDrafts() {
    return this.get({ endpoint: 'drafts' });
  }

  // Authenticated - @Private overrides class-level @Public
  @Private()
  createPost(data: any) {
    return this.post({ endpoint: 'create', data });
  }
}

export default new PostsService();

Priority Order

Decorators follow this priority:

  1. @Private() (Highest) - Always requires authentication
  2. @Public() method-level - Skips authentication
  3. @Public() class-level - All methods skip auth (unless overridden)
  4. Default - Requires authentication
@Public()
class ExampleService extends ApiService {
  // Public (class-level)
  method1() { }

  // Public (method-level override)
  @Public()
  method2() { }

  // Private (method-level override)
  @Private()
  method3() { }  // ← Token will be sent!
}

⚙️ Framework Setup for Decorators

React (Vite)

1. Install plugin:

npm install --save-dev @babel/plugin-proposal-decorators

2. Enable in jsconfig.json or tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

3. Configure vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [
          ['@babel/plugin-proposal-decorators', { legacy: true }]
        ]
      }
    })
  ],
});

4. Restart dev server:

npm run dev

Next.js

1. Install plugin:

npm install --save-dev @babel/plugin-proposal-decorators

2. Enable in tsconfig.json or jsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

3. Option A - Using Babel (Recommended):

Create .babelrc in project root:

{
  "presets": ["next/babel"],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }]
  ]
}

3. Option B - Using SWC:

Update next.config.js:

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    swcPlugins: [
      ['@swc/plugin-decorators', { legacy: true }]
    ]
  }
}

module.exports = nextConfig;

4. Restart Next.js:

npm run dev

TypeScript (Node.js)

Enable in tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

📖 API Reference

HTTP Methods

All services have these methods:

class YourService extends ApiService {
  // GET request
  getUsers() {
    return this.get({ endpoint: 'list', params: { page: 1 } });
  }

  // POST request
  createUser(data: any) {
    return this.post({ endpoint: 'create', data });
  }

  // PUT request (replace)
  replaceUser(id: string, data: any) {
    return this.put({ endpoint: id, data });
  }

  // PATCH request (update)
  updateUser(id: string, data: any) {
    return this.patch({ endpoint: id, data });
  }

  // DELETE request
  deleteUser(id: string) {
    return this.delete({ endpoint: id });
  }
}

Request Configuration

interface RequestConfig {
  endpoint: string;                  // Required - API endpoint
  params?: Record<string, any>;      // Query parameters
  data?: any;                        // Request body (POST/PUT/PATCH)
  version?: string;                  // Override service version
  isPublic?: boolean;                // Skip authentication
  includeHeaders?: boolean;          // Return headers in response

  // Mock options
  isMock?: boolean;                  // Use mock for this request
  mockData?: T;                      // Mock response data
  mockStatus?: number;               // Mock status code (default: 200)

  // Raw axios config
  config?: AxiosRequestConfig;
}

Example:

// With query parameters
await service.get({
  endpoint: 'search',
  params: { q: 'servios', limit: 10 }
});
// GET /users/v1/search?q=servios&limit=10

// With custom version
await service.post({
  endpoint: 'create',
  version: 'v3',
  data: { name: 'John' }
});
// POST /users/v3/create

// Include response headers
const response = await service.get({
  endpoint: 'me',
  includeHeaders: true
});
console.log(response.headers['x-rate-limit']);

🔐 Token Management

Automatic Token Handling

Servios automatically:

  1. Adds Authorization: Bearer {token} to requests
  2. Refreshes token on 401 errors
  3. Queues failed requests and retries after refresh
  4. Calls logout() if refresh fails

Manual Token Management

import {
  setToken,
  getToken,
  removeToken,
  setRefreshToken,
  getRefreshToken,
  removeRefreshToken,
} from 'servios';

// Access token
setToken('eyJhbGc...');
const token = getToken();
removeToken();

// Refresh token
setRefreshToken('refresh_xyz...');
const refresh = getRefreshToken();
removeRefreshToken();

Custom Token Storage

import { configureToken } from 'servios';

configureToken({
  storage: 'cookie',  // or 'localStorage' or 'sessionStorage'
  tokenKey: 'accessToken',  // default

  cookieOptions: {
    path: '/',
    secure: true,
    expires: 7,  // days
    sameSite: 'Strict',
  },

  // Separate storage for refresh token
  refreshToken: {
    tokenKey: 'refreshToken',
    storage: 'cookie',
    cookieOptions: {
      expires: 30,  // days
    },
  },
});

🎭 Mock Support

Global Mock (Development)

class UserService extends ApiService {
  constructor() {
    super({
      serviceName: 'users',
      useMock: import.meta.env.DEV,  // Mock in development
      mockDelay: 1500,  // Simulate network delay
    });
  }

  getUsers() {
    return this.get({
      endpoint: 'list',
      mockData: {
        users: [
          { id: '1', name: 'John Doe' },
          { id: '2', name: 'Jane Smith' },
        ],
      },
    });
  }
}

Per-Request Mock

getUser(id: string) {
  return this.get({
    endpoint: id,
    isMock: true,
    mockData: { user: { id, name: 'Test User' } },
    mockStatus: 200,
  });
}

📝 TypeScript Support

Full type safety with generics:

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserResponse {
  user: User;
}

class UserService extends ApiService {
  async getUser(id: string): Promise<UserResponse> {
    return this.get<UserResponse>({ endpoint: id });
  }
}

// Usage with full type inference
const { user } = await userService.getUser('123');
console.log(user.name);  // ✅ TypeScript knows this is a string

🛠️ Advanced Features

Custom Error Transform

configureBaseService({
  baseURL: 'https://api.example.com',

  transformError: (error) => {
    // Custom error structure
    return {
      success: false,
      message: error.response?.data?.message || 'Network error',
      status: error.response?.status,
      code: error.response?.data?.code,
    };
  },
});

Skip Token Refresh on Specific URLs

By default, Servios attempts to refresh the token on any 401 response. Use skipRefreshOn to prevent refresh attempts on specific paths (e.g., login, register).

Accepts an array of strings (substring match) or RegExp patterns.

Note: The URL matched against is the axios request path built by Servios (e.g., auth/v1/login), not the browser's window.location. It does not include the baseURL.

For example, a service with serviceName: 'auth', version: 'v1', and endpoint: 'login' produces the path auth/v1/login.

configureBaseService({
  baseURL: 'https://api.example.com',

  // String - skips refresh if the request path includes the string
  skipRefreshOn: ['auth/v1/login', 'auth/v1/register'],
});
configureBaseService({
  baseURL: 'https://api.example.com',

  // RegExp - more flexible matching (useful when version may vary)
  skipRefreshOn: [/auth\/.*\/login/, /public\/.*/],
});
configureBaseService({
  baseURL: 'https://api.example.com',

  // Mixed
  skipRefreshOn: ['auth/v1/login', /public\/.*/],
});

When a URL matches, the 401 error is passed directly to transformError without any refresh attempt.


Custom Retry Status Codes

configureBaseService({
  baseURL: 'https://api.example.com',
  retryOnStatusCodes: [401, 403, 503],  // Retry on these codes
});

Access Axios Instance

class CustomService extends ApiService {
  constructor() {
    super({ serviceName: 'custom' });

    // Add custom interceptor
    const axios = this.getAxiosInstance();
    axios.interceptors.request.use((config) => {
      config.headers['X-Custom-Header'] = 'value';
      return config;
    });
  }
}

📚 Complete Example

// config/api.ts - Global setup
import { configureBaseService, configureToken } from 'servios';

configureToken({
  storage: 'cookie',
  refreshToken: {
    storage: 'cookie',
    cookieOptions: { secure: true, expires: 30 },
  },
});

configureBaseService({
  baseURL: import.meta.env.VITE_API_URL,

  async refreshToken() {
    const res = await fetch('/auth/refresh', { credentials: 'include' });
    return await res.json();
  },

  logout: () => (window.location.href = '/login'),

  transformError: (err) => ({
    message: err.response?.data?.message || 'Error',
  }),
});
// services/UserService.ts
import { ApiService, Public } from 'servios';

interface User {
  id: string;
  name: string;
  email: string;
}

class UserService extends ApiService {
  constructor() {
    super({ serviceName: 'users', version: 'v2' });
  }

  @Public()
  async getPublicUsers(): Promise<{ users: User[] }> {
    return this.get({ endpoint: 'public/list' });
  }

  async getMe(): Promise<{ user: User }> {
    return this.get({ endpoint: 'me' });
  }

  async updateProfile(data: Partial<User>): Promise<{ user: User }> {
    return this.patch({ endpoint: 'profile', data });
  }
}

export default new UserService();
// services/OrgService.ts
import { ApiService, Public } from 'servios';

@Public()
class OrgService extends ApiService {
  constructor() {
    super({ serviceName: 'org' });
  }

  getOrgs() {
    return this.get({ endpoint: 'list' });
  }

  getOrgById(id: string) {
    return this.get({ endpoint: `${id}` });
  }
}

export default new OrgService();
// components/Profile.tsx
import userService from '@/services/UserService';
import orgService from '@/services/OrgService';

async function loadData() {
  try {
    // Authenticated request
    const { user } = await userService.getMe();

    // Public request
    const { orgs } = await orgService.getOrgs();

    console.log(user, orgs);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

🎯 Best Practices

1. One Global Configuration

// ✅ Good - Configure once
configureBaseService({ baseURL: 'https://api.example.com' });

// ❌ Bad - Don't configure multiple times

2. Use ApiService for App Services

// ✅ Good - Inherits global config
class UserService extends ApiService {
  constructor() {
    super({ serviceName: 'users' });
  }
}

// ❌ Bad - Manual config everywhere
class UserService extends BaseService {
  constructor() {
    super({
      baseURL: 'https://api.example.com',
      getAccessToken: () => localStorage.getItem('token'),
      // ... repeat for every service
    });
  }
}

3. Use Decorators for Public Endpoints

// ✅ Good - Clean and declarative
@Public()
class PublicService extends ApiService { }

class MixedService extends ApiService {
  @Public()
  getPublicData() { }
}

// ❌ Okay - More verbose
class Service extends ApiService {
  getPublicData() {
    return this.get({ endpoint: 'data', isPublic: true });
  }
}

4. Export Service Instances

// ✅ Good - Single instance
export default new UserService();

// ❌ Bad - Export class
export class UserService extends ApiService { }

5. Type Your Responses

// ✅ Good - Type-safe
async getUser(id: string): Promise<{ user: User }> {
  return this.get<{ user: User }>({ endpoint: id });
}

// ❌ Bad - No types
async getUser(id: string) {
  return this.get({ endpoint: id });
}

🔗 Links


📄 License

MIT © Arif Hasanov


🤝 Contributing

Contributions welcome! See CONTRIBUTING.md.