Papupata Documentation

Guide: interacting with express

Overview

It is not uncommon to want to integrate papupata into an existing express application, whether it is to use the middleware or just using papupata to model the APIs implemented with express.

Table of contents

The basics

Papupata connects itself straight to the express app, or you can provide it an express router that you can embed anywhere you wish in your express application. The timing of things depends on the configuration setting autoImplementAllAPIs: if it is enabled (default in papupata 2.x), the APIs are added to the app or router when the configuration happens, if disabled, they are added when they are implemented.

Any middleware before and after the attachment point are used as normal -- whether they are on the router or the app.

Below are examples that hopefully clarify how the different variants work; before and routerBefore are run before the implementation for api, after and routerAfter after.

App, no autoImplementAllAPIs:

const app = express()
API.configure({app, autoImplementAllAPIs: false})
app.use(before)
api.implement(implementation)
app.use(after)

Router, no autoImplementAllAPIs:

import express, {Router} from 'express'
const router = Router()
API.configure({router, autoImplementAllAPIs: false})

router.use(routerBefore)
api.implement(implementation)
router.use(routerAfter)

const app = express()
app.use(before)
app.use(router)
app.use(after)

App, using autoImplementAllAPIs:

const app = express()
app.use(before)
API.configure({app, autoImplementAllAPIs: true})
app.use(after)
api.implement(implementation)

Router, using autoImplementAllAPIs:

import express, {Router} from 'express'
const router = Router()

router.use(routerBefore)
API.configure({router, autoImplementAllAPIs: true})
router.use(routerAfter)

const app = express()
app.use(before)
app.use(router)
app.use(after)

api.implement(implementation)

Where on the app should papupata implementation be?

In an app with both papupata and non-papupata routes it should usually be the case that papupata route implementations take place before express routes. This helps ensure that the APIs are implemented according to the API declarations, without a plain express implementation taking over.

Things aren't always quite that simple though, since ambiguous routing rules and complicated middleware interactions can add difficult dependencies on implementation order. See Implementing APIs declared with just express for some ideas on what could be done in this case.

How about timing with middleware? If you have middleware that is common with the rest of you application then it makes sense to just set it all up for all of the routes. Middleware exclusive to papupata routes could be included on its router, or as papupata middleware. The advantage of using papupata middleware is that it is only used whenever an actual papupata API is called, not just for being at a specific path. Express-only middleware you'll probably want to add after the papupata routes so it ends up being bypassed outside of error cases.

Error handling

Any exceptions thrown in papupata implementations and middleware (unless handled otherwise) are passed as normal to express error handling middleware.
api.implement(() => { throw new Error('Oops') })

app.use(papupataRouter)
app.use((err, req, res, next) => {
  res.status(500)
  log.error(err)
  res.send('An error happened')
})

Route-specific middleware

You can apply route-specific middleware using the implementWithExpressMiddleware method.

api.implementWithExpressMiddleware([myExpressMiddleware], implementation)

If you need to combine express middleware with papupata middleware (which can manipulate the response after the route implementation), you can convert express middleware to papupata middleware using the convertExpressMiddleware function.

import {convertExpressMiddleware} from 'papupata'
api.implementWithExpressMiddleware(
  [myPapupataMiddleware, convertExpressMiddleware(myExpressMiddleware)],
  implementation
)

See the middleware guide for more information

Implementing APIs declared with just express

While much of the benefit to using papupata comes from using it to implement APIs, sometimes with existing applications it can be difficult to convert existing APIs to use it because of complicated middleware or routing considerations.

In these situations you can still use papupata to declare the API, and then use your old implementation. At its simplest you just implement the API you have declared and that is it. If you want to utilize the typescript types, it is also possible to an extent.

Older styles are still supported in later versions, just not preferred.
const api = API.declareGetAPI('/path/:id')
  .params({id: String}})
  .query({search: String})
  .body<string>()
  .response<string>()

app[api.method](api.path, (req, res, next) => {
  const typedRequest = req as typeof api.RequestType
  const response: typeof api.ResponseType = await calculateResponse()
  res.send(response)
})

The main caveat is that boolean query parameters are typed as booleans when they are just strings in express. If this is a problem, you can use a helper type to convert the request to express style using a helper type such as the following:

type PapupataToExpressRequest<T> = T extends { query: infer U }
  ? Omit<T, 'query'> & { query: { [t in keyof U]: string } }
  : T

type ExpressRequest = PapupataToExpressRequest<typeof api.RequestType>

If you've opted to use the autoImplementAllAPIs setting (enabled by default in papupata 2.x), any routes declared in papupata are set up to return HTTP 501 not implemented, assuming you configure papupata to your express application. This of course is undesirable when you actually want papupata to ignore the request. If you want the benefits of the setting anyway, there is a way around it; individual route implementations can return a special token value papupata.skipHandlingRoute to indicate that routing is to continue onwards.

import {skipHandlingRoute} from 'papupata'

api.implement(() => skipHandlingRoute)
app[api.method](api.path, (req, res, next) => {
  // this is where calls to api end up
})

In papupata 2.x, you can also disable the auto-implementation for specific APIs within their declaration.

const api = API.declareGetAPI('/path', {}, { disableAutoImplement: true })

As another alternative you could implement your express routes before papupata routes, but that could end up with mismatched API declarations and implementations so doing so is not without issues, either.