Papupata Documentation

Guide: implementing APIs

Overview

When creating a server with papupata, the one thing you want is to be able to do is implementing the APIs. This guide help you understand how that is done.

Table of contents

Prerequisites

Before starting this guide, should have an API declaration set up to serve requests. This is covered in the setup guide. You should also know how APIs are declared, see Declaring APIs for more details.

Examples in this guide will use routes declared like this:

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<{ name: string}>()
  .response<{status: string }>()

The basics

When it comes to just implementing APIs, it is quite simple:

complexAPI.implement(() => ({ status: 'hello' })

Of course, in practice you'll want to utilize the parameters sent to the API, and possibly do asynchronous things. Luckily things like that are still quite easy.

complexAPI.implement(async req => ({
  await doSomethingComplex(req.params.id)
  return {status: 'ok!'}
})

So what exactly makes this interesting? Types! While you still access params, query and body from the request as you would with express, everything is typed. You cannot access query and path parameters that do not exist, the body has an explicit type, and even the value you return as the response is typed to ensure that whatever you return matches the declaration.

The implementation is given three parameters: the express request modified for types, express response and the declaration for the route that is being implemented. The declaration is more useful in middleware, but sometimes you can re-use implementations for multiple routes in which case it can be useful to be able to access the route options and to construct an URL to the same API, with different parameters.

Passing the input data to other functions

It is often beneficial to split an implementation into multiple functions. But that can cause tricky type issues.

complexAPI.implement(myImplementation)

function myImplementation(req: ??): ?? {
  // What are the types here meant to be?
}

A simple but somewhat naive way to solve this problem would be to copy the types from the API declaration. If things don't match, typescript compiler will give you error messages and that'll work. But doing so can cause a bunch of extra work whenever things change, since there can be many types that need to be changed.

The smart way in most cases is to use types provided by papupata. Since objects cannot export types the syntax is a bit odd as typeof is needed, but there does not seem to be a cleaner way to handle this.

complexAPI.implement(myImplementation)

function myImplementation(req: typeof complexAPI.RequestType): typeof complexAPI.ResponseType {
  // Now we have explicit types!
}

For more information about accessing data about the APIs, see the metadata guide

Middleware

You can not only configure an API declaration to include middleware that is applied to all APIs within it, but individual APIs can have their own middleware as well. Both express and papupata middleware are supported; when both are present, express middleware is always run first. If this is a problem, it is easy to create a wrapper that runs express middeware as papupata middleware.

complexAPI.implementWithExpressMiddleware([handleAuthentication], implementation)
complexAPI.implementWithPapupataMiddleware([logRequests], implementation)
complexAPI.implementWithMiddleware({
  express: [handleAuthentication],
  papupata: [logRequests]
}, implementation)

See the middleware guide for more information about middleware.

Not actually implementing routes

Sometimes routing can work in ways where there are false positives. You might for example only want to run API actions with certain headers present, such as

Accept: application/json

Usually you'd want to do this kind of filtering with a middleware, but perhaps you have an API with unique behavior. You can have the implementation indicate that routing should be restarted past the API you are in, accomplishing what you can with express by calling next in the implementation.

import {skipHandlingRoute} from 'papupata'
complexAPI.implement(req => {
  if (!req.headers['accept']?.includes('application/json')) {
    return skipHandlingRoute
  }
  // actual implementation here
}

Error handling

Papupata does not do much in terms of error handling. Any exceptions thrown in the implementations can be caught middleware, but that not happen, the error is passed to express.

In other words, you'll want to have either express or papupata middleware to deal with the errors. There is no type information for errors at this time.

function errorMiddleware(req, res, _route, next) {
  try {
    await next()
  } catch(err) {
    console.error(err.stack || err.message || err)
    res.status(500)
    return 'Something went boom'
  }
}

complexAPI.implementWithPapupataMiddleware([errorMiddleware], () => { throw new Error() })

Conclusion

You now know enough to implement all kinds of APIs with papupata, at least for simple use cases.

Understanding middleware can be quite helpful for dealing with more complex cases, or if you already have an application using express, you might want to learn more about how express and papupata interact.

Tests are an essential part of any serious code base, so learning about testing papupata APIs is sure to be useful, too.

On the other hand, since you now know how to implement the APIs, maybe this would be a good moment to learn about how to call them? Setting up papupata client and calling APIs with papupata lead towards that direction.