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 🙏

© 2024 – Pkg Stats / Ryan Hefner

integro

v0.5.0

Published

[![npm version](https://img.shields.io/npm/v/integro)](https://www.npmjs.org/package/integro) [![npm bundle size](https://img.shields.io/bundlephobia/min/integro)](https://bundlephobia.com/package/integro) [![npm bundle size](https://img.shields.io/bundle

Downloads

982

Readme

Integro

npm version npm bundle size npm bundle size GitHub Actions Workflow Status types license

API integration with E2E integrity. Node API service with automatic client-side type safety.

Warning

Integro is under active development and breaking changes may occur at any time. For this reason, we recommend pinning the exact version of integro in your package.json file.

Installation

Install integro in your server app and client app (optional on the client side, see Re-exporting createClient).

npm install integro

Getting started

Server-side

First, create your server-side app using plain object style. No need to worry about routes.

// app.ts

export const app = {
  greetings: {
    sayHi: (name: string) => `Hi, ${name}!`,
    sayBye: (name: string) => `Bye, ${name}!`
  }
};

export type App = typeof App;

Start up the server with one line:

// server.ts

import { createController } from 'integro';
import { createServer } from 'node:http';
import { app } from './app';

createServer(createController(app)).listen(8000);

Client-side

Create the client-side api proxy object. When using in a browser, createClient must be imported from 'integro/client'. If your client is in node or bun, then createClient may be imported from either 'integro' or 'integro/client'.

// api.ts

import { createClient } from 'integro/client';
import type { App } from '@repo/api';

export const api = createClient<App>('http://localhost:8000');

Use your fully type-safe api object.

// sayHi.ts

import { api } from './api.ts';

const res = await api.greetings.sayHi('Gerald'); // 'Hi, Gerald!'

Re-exporting createClient

Wouldn't be easier if your client app didn't need to depend on integro? Of course! Let's make it happen. Simply re-export a typed version of integro's createClient function:

// Server repo

import { createClient } from 'integro/client';
import type { app } from './app';

export const createApiClient = createClient<typeof app>;
// Client repo

import type { createApiClient } from '@repo/api';

export const api = createApiClient('http://localhost:8000');

Type safety

Integro provides end-to-end type safety out of the box. That's the whole point!

An integro app object must contain only functions and nested app objects, which must contain only functions and nested app objects, and so on.

Example:

// app.ts

export const app = {
  version: () => '1.0.0',
  artists: {
    list: () => orm.artist.getAll(),
    create: (artist: Artist) => orm.artist.create(artist),
    get: (id: string) => orm.artist.get(id),
    delete: (id: string) => orm.artist.delete(id),
    update: (id: string, artist: Artist) => orm.artist.update(id, artist),
    photos: {
      list: (artistId: string) => orm.artist.get(artistId).photo.getAll(),
      get: (id: string) => orm.photo.get(id),
    },
  },
};

On the client side, methods are fully typed and converted to async functions. Type safety example

Server app type

Typing your app object is optional, but it can help find and fix issues early. There are two options for this:

import { IntegroApp } from 'integro';

export const app = {
  /* ... */
} satisfies IntegroApp;

... or ...

import { defineApp } from 'integro';

export const app = defineApp({
  /* ... */
});

Lazy loading

If your server app is large, it may be expensive to initialize it all at once. In this case, you can use integro's unwrap() helper function along with dynamic imports.

// artists.ts
export const artists = {
  list: () => orm.artist.getAll(),
  create: (artist: Artist) => orm.artist.create(artist),
  get: (id: string) => orm.artist.get(id),
  delete: (id: string) => orm.artist.delete(id),
  update: (id: string, artist: Artist) => orm.artist.update(id, artist),
}
// photos.ts
export const photos = {
  list: (artistId: string) => orm.artist.get(artistId).photo.getAll(),
  get: (id: string) => orm.photo.get(id),
}
// app.ts
import { unwrap } from 'integro';
import { artists } from 'artists';

export const app = {
  version: () => '1.0.0',
  artists, // regular, non-lazy import
  photos: unwrap(() => import('./photos').then(module => module.photos)), // lazy import
}

The client object is typed as if using regular object nesting:

Type safety with lazy loading example

Authentication and authorization

createClient accepts a requestInit option for overriding most Response properties, including headers.

// Client

import { createClient } from 'integro/client';
import type { App } from '@repo/api';
import { getCurrentAuthToken } from './auth';

export const api = createClient<App>('http://localhost:8000', {
  requestInit: () => ({
    headers: {
      'Authorization': getCurrentAuthToken()
    }
  })
});

Fine-grained authentication/authorization guard

In addition to usage for lazy loading, the unwrap function can also be used to inspect the request object.

import { unwrap } from 'integro';

export const app = {
  admin: unwrap(request => {
    const token = request.headers.get('Authorization');

    if (!isAdmin(token)) {
      throw new Error('User is not authenticated');
    }

    return {
      getUser: (id: string) => orm.users.getById(id),
      listUsers: () => orm.users.getAll(),
    };
  }),
};

Response headers

The respondWith helper allows you to customize the response headers.

Here is an example of rudimentary login/logout endpoints:

import cookie from 'cookie';
import { respondWith } from 'integro';

export const app = {
  auth: {
    login: (username: string, password: string) => {
      if (username && password) {
        const headers = new Headers();

        if (!isValid(username, password)) throw new Error('No match!');

        headers.set('Set-Cookie', cookie.serialize('session', username));

        return respondWith(undefined, { headers });
      } else {
        throw new Error('Not authenticated');
      }
    },
    logout: () => {
      const headers = new Headers();

      headers.set('Set-Cookie', cookie.serialize('session', '', { expires: new Date(0) }));

      return respondWith(undefined, { headers });
    },
  },
};

Cross-Origin Resource Sharing (CORS)

createClient accepts header properties to allow for CORS, while the server can be configured according to your chosen framework.

// Server

createServer((req, res) => {
  res.setHeader('access-control-allow-credentials', 'true');
  res.setHeader('access-control-allow-headers', 'Content-Type');
  res.setHeader('access-control-allow-origin', 'http://localhost:5173');
  res.setHeader('access-control-max-age', '2592000');

  if (new URL(req.url ?? '', 'https://localhost').pathname === '/api') {
    return createController(app)(req, res);
  }

  res.end();
}).listen(8000);
// Client

import { createClient } from 'integro/client';
import type { App } from "@repo/my-server";

export const api = createClient<App>("http://localhost:8000", {
  requestInit: {
    headers: {
      credentials: 'include'
    }
  }
});

Server-side validation

Type safety can help you avoid most problems at development and build time, but what about protecting the server against unforeseeable bugs or bad actors?

Server-side validation is currently not included out of the box, but there are many great options already available to add validation with little effort.

With Prisma

Prisma includes validation, so you can simply export many prisma methods directly:

import { prisma } from './prisma';

export const app = {
  artist: {
    findFirst: prisma.artist.findFirst,
    findMany: prisma.artist.findMany,
  },
};

With Typia

Typia includes an assertParameters() guard which validates the input parameters to a given function based only on TS types:

import typia from 'typia';

export const app = {
  repeatString: assertParameters(
    (text: string, times: number) => Array(times).fill(text).join(', ')
  )
};

With Zod

import z from 'zod';

export const app = {
  repeatString: z.function().args(z.string(), z.number()).implement(
    // text and times types are inferred via zod
    (text, times) => Array(times).fill(text).join(', ')
  ),
};

Framework agnostic

Integro's createController() works with servers that accept handlers in the form (request: IncomingMessage, response: ServerResponse) => void (such as node:http, express) as well as the Fetch API style (request: Request) => Response (such as bun).

Node's built-in http package

Any route:

import { createController } from 'integro';
import { createServer } from 'node:http';
import { app } from './app';

// Any route
createServer(createController(app)).listen(8000);

Specific route:

import { createController } from 'integro';
import { createServer } from 'node:http';
import { app } from './app';

// Specific route
createServer((req, res) => {
  if (new URL(req.url ?? '', 'https://localhost').pathname === '/api') {
    return handle(req, res);
  }

  res.end();
}).listen(8000);

Bun's built-in serve function

Any route:

import { serve } from 'bun';
import { createController } from 'integro';
import { app } from './app.js';

serve({
  port: 8000,
  fetch: createController(app)
});

Specific route:

import { serve } from 'bun';
import { createController } from 'integro';
import { app } from './app.js';

serve({
  port: 8000,
  fetch: (req) => {
    if (new URL(req.url).pathname === '/api') {
      return createController(app)(req);
    }

    return Response.error();
  }
});

Express

Any route:

import { app } from './app';
import { createController } from 'integro';
import express from 'express';

express()
  .use(createController(app))
  .listen(8000);

Specific route:

import { app } from './app';
import { createController } from 'integro';
import express from 'express';

const handler = createController(app);

express()
  .options('/api', (req, res, next) => {
    handler(req, res);
    next();
  })
  .post('/api', (req, res, next) => {
    handler(req, res);
    next();
  })
  .listen(8000);

Next.js

With app router:

// src/api/route.ts

import { createController } from 'integro';
import { app } from './app';

export const POST = createController(app);

Client-side recipies

You can retrieve the path name of an integro client method by using the well-known symbol Symbol.toStringTag. This is useful for cases where you need to dynamically get the path at runtime.

const path = api.artists.list[Symbol.toStringTag];
console.log(path); // => 'artists.list'

Here are a couple examples where Symbol.toStringTag proves useful for using integro with React querying hooks:

With Vercel's SWR

// api.ts

import type { createApiClient } from '@repo/api';
import { AnyClientMethod } from 'integro/client';
import useSWR from 'swr';

export const api = createApiClient("http://localhost:8000/api", {
  requestInit: { credentials: 'include' }
});

export const useIntegroSWR = <Fn extends AnyClientMethod>(fn: Fn, ...args: Parameters<Fn>) =>
  useSWR<Awaited<ReturnType<Fn>>>([fn[Symbol.toStringTag], ...args], () => fn(...args));


// Artists.tsx
import { api, useIntegroSWR } from '../api';

export const Artists = () => {
  const { data } = useIntegroSWR(api.artists.list);

  return (
    <div>
      {data?.map((artist) => (
        <p key={artist.name}>
          {artist.name}: {artist.instruments?.join(",")}
        </p>
      ))}
    </div>
  );
};


// Artist.tsx
import { api, useIntegroSWR } from '../api';

export const Artist = () => {
  const { data } = useIntegroSWR(api.artists.get, 'monk');

  return (
    <p>
      {artist.name}: {artist.instruments?.join(",")}
    </p>
  );
};

With Tanstack Query

// api.ts

import type { createApiClient } from '@repo/api';
import { useQuery } from '@tanstack/react-query';
import { AnyClientMethod } from 'integro/client';

export const api = createApiClient("http://localhost:8000/api", {
  requestInit: { credentials: 'include' }
});

export const useIntegroQuery = <Fn extends AnyClientMethod>(fn: Fn, ...args: Parameters<Fn>) =>
  useQuery<Awaited<ReturnType<Fn>>>({
    queryKey: [fn[Symbol.toStringTag], ...args],
    queryFn: () => fn(...args)
  });


// Artists.tsx
import { api, useIntegroQuery } from '../api';

export const Artists = () => {
  const { data } = useIntegroQuery(api.artists.list);

  return (
    <div>
      {data?.map((artist) => (
        <p key={artist.name}>
          {artist.name}: {artist.instruments?.join(",")}
        </p>
      ))}
    </div>
  );
};


// Artist.tsx
import { api, useIntegroQuery } from '../api';

export const Artist = () => {
  const { data } = useIntegroQuery(api.artists.get, 'monk');

  return (
    <p>
      {artist.name}: {artist.instruments?.join(",")}
    </p>
  );
};

License

MIT