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

rickety

v4.1.0

Published

minimal typescript rpc framework

Downloads

182

Readme

:scroll: rickety

minimal typescript rpc library

Try out the example project to experiment with a working setup (including tests)

Install

$ npm install rickety

Usage

import {DefaultClient, Endpoint} from "rickety";
const myAPI = new DefaultClient();

const userByID = new Endpoint<number, User>({
    client: myAPI,
    path: "/api/v1/...",
});
app.use(
    userByID.handler(async (id) => {
        // ...
        return user;
    });
);
const user = await userByID.call(id);

Endpoint

An endpoint's call function sends requests using the configured options. It returns a promise which may be rejected if there is an issue with the request process or if the status is unexpected.

const response = await endpoint.call(request);

Request handlers contain the server code that transforms requests into responses. Both express' req and res objects are passed to the function which makes it possible to implement custom behavior like accessing and writing headers.

app.use(
    endpoint.handler(async (request, req, res) => {
        // ...
        return response;
    });
);

Endpoints expose their configuration through readonly public values which can be accessed from the instance.

const method = endpoint.method; // POST

The endpoint's request and response types can also be accessed using typeof on two special members. Using them by value with produce an error.

type Request = typeof endpoint.$req;
type Response = typeof endpoint.$res;

Config

const endpoint = new Endpoint({
    client: Client;
    path: string;
    method?: string;
    expect?: number | number[];
    isRequest?: (req: any) => boolean;
    isResponse?: (res: any) => boolean;
    strict?: boolean;
});

| | | | -- | -- | | client | Client is used to send the requests and can be shared by multiple endpoints. More info here. | | method | HTTP method used when handling and making requests. Defaults to POST if not configured. | | path | Required URL path at which the handler will be registered and the requests will be sent. | | expect | Expected returned status code(s). By default, anything but 200 is considered an error. This value is only used for making requests and has no influence on the handler (which will return 200 by default). | | isRequest isResponse | Type checking functions run before and after serializing the objects in both client and server. By default any value will be considered correct. | | strict | Flag to enable strict JSON marshalling/un-marshalling. By default "raw" strings are detected and handled correctly. In strict mode, they would cause a parsing error. This issue comes up if a server is returning a plain message str. Since it is not valid JSON it cannot be parsed without extra steps. The correct format for a JSON string surrounds it with double quotes "str". |

Client

Clients are responsible for sending requests and receiving responses.

Rickety is released with a few included clients which can be imported using the rickety/client/... path pattern.

| fetch | xhr | node | request | axios | | -- | -- | -- | -- | -- |

The fetch client is used as DefaultClient.

Clients can be extended or re-implemented to better address project requirements. For example, a client can enable caching, modify headers or append a path prefix or a domain to endpoint URLs. The only requirement for a client is that it satisfies the Client interface available in rickety/client.

The supertest client also enables easy integration tests, as detailed in the testing section.

Group

Groups allow multiple endpoints to be treated as a single construct while preserving type information.

const userByID = new Endpoint<string, User>( ... );
const promotedByUserID = new Endpoint<string, Product[]>( ... );
const allProducts = new Endpoint<Query, Product[]>( ... );

const listingPage = new Group({
    user: userByID,
    listing: {
        promoted: promotedByUserID,
        all: allProducts,
    },
});

Groups can be used inside other groups.

Groups are called using a request object with the same "shape" as its definition, but with the correct request data type in the place of the endpoints. Similarly, the response is also strictly typed and shares the same "shape" as the definition, but with response data in the place of the endpoints.

Here is an example request and response objects for the above group.

const pageData = await listingPage.call({
    user: "abc-123-xyz",
    listing: {
        promoted: "abc-123-xyz",
        all: {
            page: 3
        },
    },
});

// pageData {
//     user: {...}
//     listing: {
//         promoted: [...],
//         all: [...],
//     },
// }

The group's dynamic request and response types can also be accessed using typeof on two special members. Using them by value with produce an error.

type Request = typeof group.$req;
type Response = typeof group.$res;

Testing

An endpoint/group's call function can be spied on to test behavior with mocked return values or assert on how it is being called.

import {getUserData} from "../endpoints";
import {Homepage} from "../frontend/components";

test("homepage fetches correct user data", () => {
    const spy = jest.spyOn(getUserData, "call");
    spy.mockReturnValue({ ... });

    mount(<Homepage ... />);

    expect(spy).toHaveBeenCalledWith( ... );
});

The express app instance can be "linked" to test handler behavior.

import {SupertestClient} from "rickety/client/supertest";

import {app} from "../backend/app";
import {database} from "../backend/database";
import {client} from "../client";
import {createUserByEmail} from "../endpoints";

SupertestClient.override(client, app);

test("new user is created in the database", async () => {
    const spy = jest.spyOn(database, "createUser");
    spy.mockReturnValue({ ... });

    await createUserByEmail( ... );

    expect(spy).toHaveBeenCalledWith( ... );
});

This pattern also enables integration tests which involve both client and server code.

import {SupertestClient} from "rickety/client/supertest";
import {mount} from "enzyme";

import {app} from "../backend/app";
import {database} from "../backend/database";
import {client} from "../client";
import {SignUp} from "../frontend/components";

SupertestClient.override(client, app);

test("should refuse duplicate email addresses", async () => {
    const spy = jest.spyOn(database, "createUser");
    spy.mockReturnValue({ ... });

    const wrapper = mount(<SignUp ... />);
    const submit = wrapper.find('button');
    submit.simulate('click');

    expect(wrapper.find(".error")).toContain("...");
});

License

MIT