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

rest-contracts-example

v1.1.0

Published

An example using rest-contracts

Downloads

9

Readme

REST-Contracts

Minimalist TypeScript Libraries for Defining & Implementing REST APIs

The REST-Contracts set of packages were designed for web services that run TypeScript on both their back-end (API server/provider) and front-end (API client/consumer), and want a light-weight way to:

  • specify the inputs and outputs of each API call
  • automatically generate typed API client functions
    • with types for each function's parameters so that TypeScript can enforce the contract
    • with each function returning a Promise for the result type specified in the contract
  • use TypeScript to simplify implementing the API service while reducing errors
    • parameters arrive to your code with the types specified in the contract
    • the type checker verifies that your implementation returns the type specified in the contract

REST-contracts uses auto-complete to help make writing APIs easy.

First, you can use auto-complete to pick an HTTP REST method.

Intellisense shows you the Method options avaialble

Next, you choose the specify the path. Any path parameters should appear between slashes and be preceded by a colon (":").

Intellisense indicates where to specify the path.

Then, you specify parameters. In this example, we'll set path parameters but not query parameters.

Intellisense gives you the option to specify parameters

Finally, you specify a result type or return void.

Intellisense gives you the option to specify a result or return no result (void)

The result is a complete contract.

A complete contract for a REST GET API.

A full contract for a data type (e.g. an Excuse object) might look something like this, with a GET request for individaul objects, a GET request for multiple objects, and a PUT request to add items.

// from rest-contracts-example/src/excuse-contract.ts
export const Get =
  API.Get
  .Path('/excuses/:id/')
  .PathParameters<{ id: ExcuseId }>()
  .Returns<ExcuseDbRecord>();

export const Query =
  API.Get
  .Path('/excuses/')
  .QueryParameters<{quality?: ExcuseDbRecord["quality"]}>()
  .Returns<ExcuseDbRecord[]>();

export const Put =
  API.Put
  .Path("/excuses/")
  .Body<Excuse | ExcuseDbRecord>()
  .Returns<ExcuseDbRecord>();

The Get, Query, and Put objects generated in this example contain a path and method. More importantly, the compiler attaches to these objects additional type information that defines parameters and return type of each API call. These attached types are consumed by packages that help you to implement the APIs on the server side that implement client functions to allow client code to call these APIs--providing compile-time checking of compliance with the interface contract on both sides.

REST-contracts server packages simplify correct API implementation.

To create servers implement the API in these contracts, you can currently used rest-contracts-express-server, rest-contracts-lambda, or implement your own.

// from rest-contracts-example/src/server.ts

// You'll implement your APIs by calling the implement method of this class.
// (Want to add authentication or other steps?  Just create a subclass.)
const ourApi = new RestContractsExpressServer.RestContractsExpressServer(router);

// Best to create a new file here, import the ourApi class,
// and separate implementations into different files.
const excuseMemoryDatabaseTable =
  new Map<ExcuseContract.ExcuseDbRecord["id"], ExcuseContract.ExcuseDbRecord>();

// We've merged all params into params, but req, res, & next are still available
// to you. (We've also added typings to the parameter fields of the req object!)

// Implement the Excuse Put method
ourApi.implement(ExcuseContract.Put, (body, params, req, res, next) => {
  const excuseOrExcuseDbRecord = body;
  const excuseDbRecord: ExcuseContract.ExcuseDbRecord =
    ("id" in excuseOrExcuseDbRecord && excuseOrExcuseDbRecord.id) ?
      // parameter is already an ExcuseDbRecord with an ID assigned
      excuseOrExcuseDbRecord :
      // parameters is an excuse that is not yet in the database and
      // needs to be assigned a unique ExcuseId
      {
        ...excuseOrExcuseDbRecord,
        id: ExcuseContract.ExcuseId()
      };
  excuseMemoryDatabaseTable.set( excuseDbRecord.id, excuseDbRecord );
  // Put methods return a copy of the record that they stored
  return excuseDbRecord;
});

// Implement the Excuse Get method
// (leaving out req, res, next since we're not using them)
ourApi.implement(ExcuseContract.Get, (params) =>
  excuseMemoryDatabaseTable.get(params.id)
);

// Impelment the Excuse directory-level Get, which can query on quality
ourApi.implement(ExcuseContract.Query, (params) =>
  [...excuseMemoryDatabaseTable.values()].
    // Take all exucses if no quality parameter speicfied,
    // or only those excuses with a quality matching the one queried for
    filter( values => (!params.quality) || values.quality === params.quality )
);

REST-contracts client packages build typed client functions for you.

To automatically build client functions to call the API in the contract, we have a minimal rest-contracts-browser-client, which has no dependencies so that it can run compactly in the browser, and rest-contracts-axios-client, which builds on top of axios so that it can run within node or in the browser. Since our example runs on node, it uses the latter.

// from rest-contracts-example/src/client.ts

// Add two excuses.  Excuses for what?  Not using TypeScript.
const firstExcuseStored = await excuseClient.put({
  quality: ExcuseQuality.Poor,
  description: "I don't use TypeScript I enjoy debugging type errors in production software.",
});

console.log("Put the first excuse", firstExcuseStored);

const secondExcuseStored = await excuseClient.put({
  quality: ExcuseQuality.Poor,
  description: "My nervous systems has a built-in type checker that catches errors before they become keystrokes.",
});

console.log("Put the second excuse", secondExcuseStored);
const excuseId = firstExcuseStored.id;

// Retrieve the first excuse again using get with a path parameter.
const excuse = await excuseClient.get({id: excuseId});

// Retrieve the second excuse again using get with a path parameter.
const secondExcuse = await excuseClient.get({id: secondExcuseStored.id});

// Retrieve excuses using Query (get with a query parameter)
const lameExcuses = await excuseClient.query({quality: ExcuseContract.ExcuseQuality.Poor});
console.log("Retrieved excuses:", lameExcuses);

// Retrieve excuses using Query (get with a query parameter)
const goodExcuses = await excuseClient.query({quality: ExcuseContract.ExcuseQuality.Good});
console.log("Here are all the valid excuses for not using TypeScript:", goodExcuses);
console.log("Yup, that's all of them.");

You can download the working example from the package/rest-contracts-example directory of the repository on GitHub, or download it as an npm package.

Contributing

Make sure you're using yarn version >= 1 and have the shx package installed globally.

npm install -g yarn@latest
npm install -g shx
git clone https://github.com/UppaJung/rest-contracts.git
yarn

The only thing we love more than carefully-crafted pull requests are the people who contribute them.

lerna