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

concord

v0.2.4

Published

transforms a Typescript interface into usable client / server code

Downloads

14

Readme

CircleCI

concord

concord transforms Typescript interfaces into usable client / server code.

It simplifies the process of writing clients and servers and lets you concord your code faster. Instead of describing REST APIs, concord abstracts away REST and HTTP and gives you a simple typescript interface.

Behind the scenes it uses simple HTTP POST with JSON payload and is validated using JSONSchema. The heavy lifting is done by typescript-json-schema.

Usage

  1. Create the interface file.

    interface.ts

    export interface Example {
      add: {
        params: {
          a: number;
          b: number;
        };
        returns: number;
      };
    }
  2. Compile the schema.

    concord -o ./generated node --client fetch --server koa [email protected] interface.ts
  3. Write the server code.

    server.ts

    import { ExampleServer } from './generated/server';
    
    class Handler {
      public async add(a: number, b: number): Promise<number> {
        return a + b;
      }
    }
    
    const h = new Handler();
    
    const server = new ExampleServer(h);
    server.listen(8080);
  4. Write the client code.

    client.ts

    import { ExampleClient } from './generated/client';
    
    async function main() {
      const client = new ExampleClient('http://localhost:8080');
      try {
        const x = await client.add(1, 2);
        console.log(x);
      } catch (err) {
        console.error(err);
      }
    }
    
    main();
  5. Run (make sure tsconfig.json is properly configured for node and is present in the current directory) TODO: Test this process

    tsc
    ./server.js &
    ./client.js

    Alternatively with ts-node:

    ts-node ./server.ts &
    ts-node ./client.ts

Advanced usage

Creating an npm package

Concord can create an npm package for you and publish it if instead of specifying an output dir you give it a publish target. In the following example concord will publish the generated server files to npm as [email protected]:

concord node --publish --client fetch [email protected] interface.ts

Generating code for different runtimes

The first positional argument given to the concord CLI is runtime, currently supported runtimes are node and browser.
The node node runtime supports koa and binaris servers and a fetch client.
Run concord <runtime> --help for more details.

Calling with curl

curl -X POST -H 'Content-Type: application/json' http://serverAddress:serverPort/add -d '{"a": 1, "b": 2}'

Calling with httpie

http post http://server-address/add a:=1, b:=2

Object params

Complex nested object types are supported.

Date parameter types or return type are validated by JSON schema and cast into back to Date objects when deserialized.

export interface User {
  name: string;
  createdAt: Date;
}

export interface Example {
  lastSeen: {
    params: {
      u: User;
    };
    returns: Date;
  };
}

Context

Some use cases require context to be passed on to handlers (i.e. for authentication / extracting the request IP).

There are 2 types of contexts in concord, ClientContext and ServerOnlyContext.

  • ClientContext is prepended to the client call signature and is exported as Context from the generated client file.
  • ServerOnlyContext is extracted by the server using a custom provided function that accepts a request object (depends on the runtime) and returns a context object. Handler methods receive a context which is an intersection of ClientContext and ServerOnlyContext and is exported as Context from the generated server code.

To use contexts simply add them to your interfaces file.

interface.ts

export interface ClientContext {
  token: string;
}

export interface ServerOnlyContext {
  ip: string;
}

export interface Example {
  hello: {
    params: {
      name: string;
    };
    returns: integer;
  };
}

server.ts

import * as koa from 'koa';
import { ExampleServer, Context, ServerOnlyContext } from './generated/server';

export class Handler {
  public async extractContext({ ip }: koa.Context): Promise<ServerOnlyContext> {
    return { ip };
  }

  public async hello({ token, ip }: Context, name: string): Promise<string> {
    return `Hello, ${name} from ${ip}, token: ${token}`;
  }
}

const h = new Handler();

const server = new ExampleServer(h);
server.listen(8080);

client.ts

import { ExampleClient, Context } from './generated/client';

async function main() {
  const client = new ExampleClient('http://localhost:8080');
  await client.hello({ token: 'abc' }, 'baba'); // Hello, baba from 127.0.0.1, token: abc
}

main();

Custom context

When exporting multiple interfaces from the same file you might want to use custom Context for each interface.
In order to use the custom Context interfaces specify clientContext or serverOnlyContext fields on your interface, like so:

export interface AuthInfo {
  token: string;
}

export interface User {
  name: string;
}

export interface Example {
  clientContext: AuthInfo;
  serverOnlyContext: User;
  greet: {
    params: {
      greeting: string;
    };
    returns: string;
  }
}

In order to disable a context on your interface, specify: clientContext: false or serverOnlyContext: false.

Generating only client / server

concord -o ./generated_client node --client fetch [email protected] interfaces.ts
concord -o ./generated_server node --server koa [email protected] interfaces.ts

Mounting the app with a different prefix and adding custom middleware

server.ts

// ...
import { ExampleRouter } from './generated/server';

// ... implement Handler class ...
const h = new Handler();
const router = new ExampleRouter(h);
const app = new Koa();

const baseRouter = new Router(); // koa-router
baseRouter.use('/prefix',  router.koaRouter.routes(),  router.koaRouter.allowedMethods());
app.use(baseRouter.routes());
app.use(async function myCustomMiddleware(ctx: koa.Context, next) {
  // ... implement middlware ...
});
// ... app.listen(), etc ...

Exceptions # TODO

JSON Schema attributes

Use annotations to specify JSON Schema attributes.

export interface Example {
  add: {
    params: {
      /**
      * @minimum 0
      */
      a: integer;
      /**
      * @minimum 0
      */
      b: integer;
    };
    returns: integer;
  };
}

Integers

Define integer as number, it'll be reflected in the generated JSON schema while the generated Typescript code will be typed as number.

export type integer = number;

export interface Example {
  add: {
    params: {
      a: integer;
      b: integer;
    };
    returns: integer;
  };
}

Void return type

When null is the only return type on a method, as in returns: null, it will compile to Promise<void>.

Defining returns: null | SomethingElse on a method will compile to Promise<null | SomethingElse> return type.

Binaris backend

  1. Create a new directory.

  2. Switch to new directory.

  3. Create an interface.ts file as shown above.

  4. Generate your server code with concord node --client fetch --server binaris [email protected] interface.ts -o generated (the client is here for the sake of the example and is not required).

  5. Run bn create node8 add.

  6. Implement a handler:

    function.js

    const { ExampleWrapper } = require('./generated/server');
    exports.handler = ExampleWrapper.add(async (a, b) => a + b);
  7. Export your binaris credentials: export BINARIS_ACCOUNT_ID=$(bn show accountId) BINARIS_API_KEY=$(bn show apiKey)

  8. Deploy your function with bn deploy add.

  9. Create a test file

    test.js

    const { ExampleClient } = require('./generated/client');
    
    async function main() {
      const url = `https://run.binaris.com/v2/run/${process.env.BINARIS_ACCOUNT_ID}`;
      const client = new ExampleClient(url, {
        headers: {
          'X-Binaris-Api-Key': process.env.BINARIS_API_KEY,
        },
      });
      console.log('1 + 2 =', await client.add(1, 2));
    }
    main();
  10. Run your test with node test.js, should print out 1 + 2 = 3 (That is if I can do math ;) ).

Comparison to other tools

OpenAPI (Swagger)

OpenAPI provides an easy way to write descriptive REST APIs. concord on the other hand, spares you from even thinking about REST and lets you focus on your buisness logic.

Both projects use JSON Schema for input validation. OpenAPI let's you write pure JSON schema while concord interfaces are written in typescript.

Protobuf / Thrift

protobuf and thrift have ATM more efficient serialization. They both enforce backwards compatibility better with field numbering? (TODO) In the future we could add binary serialization to concord but we default to JSON for readability.

concord provides advanced type validation with JSON schema.

concord uses mustache templates which are easy to customize to support any language / framework.