Papupata Documentation

Guide: declaring APIs

Overview

Papupata is all about implementing and invoking APIs. In order for any of that to happen, the APIs must first be declared, or modeled using the tools provided by papupata. This guide covers how the declarations are made.

Table of contents

Setting up an API declaration

The first thing you'll need is an API declaration. Simply put, it typically represents a set of APIs accessible from a single host. It could be all of the APIs of an application, or just one of them.

In order to actually use the declared APIs, it is necessary to set up configuration, but just for the purposes of declaring APIs we do not have to worry about that.

import {APIDeclaration} from 'papupata'
const API = new APIDeclaration()

All of the examples in the sections below will expect this API declaration to exist in their scope.

Your first declaration

Let's start with a really simple API. An API found at /hello, invoked with the HTTP method GET, which a string as its response.

const helloAPI = API
  .declareGetAPI('/hello')
  .response<string>()

Seems simple, enough, right? We specify the method and the path by invoking the declareGetAPI function on the API declaration, and then we declare the response type with the response method.

Bodies and responses are often complicated objects, and as such they are represented using typescript types. As we see in the example, the type of the response is passed to the response function as an explicit type parameter.

The anatomy of a declaration

API declarations often contain many things, and many of those can be modeled using papupata.

Let's start by looking at an API declaration which utilizes all of the possibilities provided by papupata

Older styles are still supported in later versions, just not preferred.
const complexAPI = API.declarePostAPI('/update/:id', routeOptions)
  .params({id: String})
  .query({author: String, notifyWatchers: Boolean}})
  .optionalQuery({timestamp: String})
  .body<string, Date>()
  .response<string, Date>()

Having easy-to-read API declarations was one of the main goals of papupata, and hopefully we've been reasonably successful. It is rare to see everything present in the example in real APIs, so it is something of an abomination.

Anyway, let's briefly go through each part

const complexAPI

You need to store APIs somewhere. Although you declare them on the API declaration object, you can not access them from there -- and you'd never be able to get the type safety if you could.

For simple cases you can store the APIs in simple variables, but if you are modeling a collection of APIs, it probably makes sense to store the APIs in an object:

const apis = {
  getOne: API.declareGetAPI(...),
  updateOne: API.declareGetAPI(...),
  deleteOne: API.declareGetAPI(...),
  find: API.declareGetAPI(...),
}

You can even nest objects to make a logical hierarchy out of them.

.declarePostAPI(

All APIs are declared using one of the methods on an API declaration instance. The naming pattern is always the same, so you can expect to find declareGetAPI, declarePutAPI and so on to be available.

At this time there is no way to define an API to support multiple methods -- you have do declare it separately for each method.

'/update/:id'

All APIs are declared using relative paths. The base URL is set up before calls can be made, and it can even include paths as well.

Path parameters must be present in the url passed to the declaration function; they are indicated by a colon before the parameter name.

Query parameters are generally speaking NOT included in the url, however you can include query-based rules in the path when APIs only differ from each other by their query parameters. See Query-based variants for more information.

, routeOptions)

Sometimes you need to add metadata to the routes. It could be relevant for the client, server, or both.

The API declaration has a number of type parameters, one of which is used to define the type of the route options.

These parameters are typically accessed either from the middleware or the function that actually makes HTTP requests.

A common use case would be to indicate the need for authentication. This allows the server to verify authentication as needed, while the client can skip obtaining the credentials.

As the example API declaration does not include options, here is another small example:

import {Request} from 'express'
const API = new APIDeclaration<Request, {requiresAuthentication: boolean}>
const api = API.declareGetAPI('/', {requiresAuthentication: false})
Older styles are still supported in later versions, just not preferred.
.params({id: String})

In addition to being declared in the URL, path parameters need to be declared in a way that lets typescript know they exist. This call to params serves that purpose.

Just create an object where the keys are the names of the path parameters and the values represent their types. See TypeMapping for more information on the types.

At this time path parameters cannot be optional; if you need to support that case, you have to declare multiple APIs that match the different cases.

Older styles are still supported in later versions, just not preferred.
.query({author: String, notifyWatchers: Boolean}})
.optionalQuery({timestamp: String})

The format for entering query parameters is the same as with path parameters. Optional parameters are supported for queries, though they have to be separated from the required ones. While you can have both required and optional parameters in any given API, the required parameters must be declared first.

Again, see TypeMapping for more information on the supported types. The only difference to path parameters in this regard is that for query parameters arrays are supported.

.body<string, Date>()

Oh boy. Let's start with something a little simpler: .body<string>(). That is what you'll usually see, and should be quite straightforward. The body for the request must be a string.

I can be any type, but in practice the transport medium (commonly json) does tend to put limitations to what can really be transferred. Functions, for example, you probably can't just pass along over APIs.

Sometimes there are types which are transformed automatically. When using JSON, dates for example are commonly just stored as a string, even if you original payload had it as a date. This creates a typing conundrum: surely you should be able to pass in a date, even if the other side will see it as a string. This is where the second type parameter comes in. The first parameter is the type of the body, as it will be seen by the server, and the second one is the type of the body the way the client application can pass it. The conversion between the two should be transparent to the user. If you only pass one type parameter, it is used for both cases.

.response<string, Date>()

Response work just like body does, except from the opposite point of view.

You can pass a single type argument and it'll be the type of there response everywhere. If you pass another one though, that is what the implementation is expected to return. Again, the conversion to the type seen by the client should be implicit, either built into the serialization process or maybe in the form of a middleware.

All declarations must end with a response. The type can be void or undefined if the API returns nothing, but the call to response must be there regardless.

Error handling

At this time papupata does not have particular support for managing errors.

You could type the response to be either the actual response or an error response, but that's essentially the extent of it.

const api = API.declareGetAPI('/path')
  .response<Data | {errorMessage: string}>()

Even so, there are certainly limitations to this. If the client request library rejects the failed requests, the type information will be lost anyway and even in success cases you have to ensure you got the data object even if it should be obvious.

This is certainly something that will be developed further in upcoming releases.

Conclusion

What we've covered includes essentially everything papupata has for declaring the APIs.

The logical next steps would be about how to actually use the declarations. If you want to call the declared APIs, head on over to setting up papupata client, or perhaps straight to calling APIs if someone else has set things up for you already.

On the other hand, if you want to implement APIs that have been declared, setting up papupata server and implementing APIs are good next guides.

Lastly, if you want to know about how to extract metadata from the declared APIs, there is a guide about metadata, too.