Serverless Contracts to the Rescue of your Fullstack Application
Tags: serverless, typescript, aws, opensource
Published: 2022-11-22 13:00:55

## Clear and steady interfaces for stable applications 🧘

Introducing a **breaking change** in the input or output of your API and not updating the frontend client might be the most common error a fullstack developper makes at the start of its career (and even later 👴).

**Interfaces** between different services are one of the weakest, if not the weakest, point in an application (right after copy pasted stackoverflow one liners 👨‍🔬).

Thats why you should put a lot of effort in defining those interfaces. However, it **can become quite repetitive**, and that's what we wanted to tackle with **Serverless contracts**.

## Serverless Contracts 📑

At [Kumo](https://dev.to/kumo), we build **fullstack** applications using **Serverless** and **TypeScript**. During our multiple projects, we encountered the problem of varying interfaces a lot. That is why we decided to build [Swarmion](https://www.swarmion.dev/), a tool to build stable serverless application at scale.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5boqk5aax3cb9v2lfzqr.png)


One of its most important feature is Serverless Contracts.

---

## What we wanted 🙏

When you want to build a tech tool, you should keep a **product approach** as much as possible. It can help you defining where the value sits and where you should be putting work.

So when we first thought about contracts, we focused on our main use case: **interfaces** between frontend and backend microservices, with the backend deployed behind API Gateway.

With that in mind, we wanted to address the folowing pain points with Serverless Contracts:

- Usable in both the **backend** and the **frontend**
- **Strong type safety** during static analysis
- **IntelliSense** in the IDE
- **Runtime input and output validation** at the backend for client-side errors handling
- **Reduced boilerplate** in provider (API) and consumer (frontend) for every handler/request
- **Human readable** definition

---

## What we built 🔨

Now, lets dive into some code, with your first **Serverless contract**

```tsx
// contracts/users-contracts/src/contracts/getUser/contract.ts
import { ApiGatewayContract } from '@swarmion/serverless-contracts';

import { pathParametersSchema, userEntitySchema } from './schemas';

const getUserContract = new ApiGatewayContract({
  id: 'users-getUser',
	integrationType: 'httpApi',
  authorizerType: undefined,
  path: '/users/{userId}',
  method: 'GET',
  pathParametersSchema,
  queryStringParametersSchema: undefined,
  headersSchema: undefined,
  bodySchema: undefined,
  outputSchema: userEntitySchema,

});

export default getUserContract;
```

```tsx
// contracts/users-contracts/src/contracts/getUser/schemas.ts
export const pathParametersSchema = {
  type: 'object',
  properties: {
    userId: { type: 'string' },
  },
  required: ['userId'],
  additionalProperties: false,
} as const;

export const userEntitySchema = {
  type: 'object',
  properties: {
    userId: { type: 'string' },
    userName: { type: 'string' },
    email: { type: 'string' },
  },
  required: ['userId', 'userName', 'email'],
  additionalProperties: false,
} as const;
```

There is a lot of things to see here:

### The getUserContract 👥

This class instance contains all the informations defining your endpoint interface:

- **Implementation details** of your endpoint:
    - A unique identifier
    - The api gateway type (V1 or V2)
    - The authorizer type (cognito, jwt, lambda or none)
- The **path and method** of your endpoint
- The different parts of the **incoming request**:
    - path parameters
    - query string parameters
    - headers
    - body
- The format of the **output** of your endpoint.

The last 2 categories are defined using JSON Schemas.

### JSON Schemas 💽

We use **JSON Schemas** for multiple reasons:

- They are a well known interface definition format in the **javascript ecosystem**.
- With the [json-schema-to-ts](https://github.com/ThomasAribart/json-schema-to-ts) lib, we can define our **TypeScript** types directly from those schemas without any duplication and provide **fully typed** helpers as we will see later.
- They allow us to perform **run time validation** with [ajv](https://ajv.js.org/).

In this contract, we define an `outputSchema` and a `pathParameterSchema`. The other options are left undefined because those inputs are not used by our endpoint.

Now that we are settled with our contract, we can start using it in our application

## Provider-side features

First, we want to define our endpoint using our contract.
With the [Serverless framework](https://www.serverless.com/), you do it by defining 2 elements: the function **trigger** and its **handler**.  Serverless contracts provide helpers for both.

### getTrigger ⚡

```tsx
// config.ts
import { getTrigger } from '@swarmion/serverless-contracts';
import { getUserContract } from '@my-app/contracts/getUserContract';

export default {
  handler: getHandlerPath(__dirname),
  events: [getTrigger(getUserContract)],
};
```

This method defines the path and method that will trigger the lambda based on the contract information.

If you define an authorizer in your contract, you can provide it there directly, and it will **prevent you to overwrite the trigger values**.

```tsx
// config.ts
import { getTrigger } from '@swarmion/serverless-contracts';
import { getUserContract } from '@my-app/contracts/getUserContract';

export default {
  handler: getHandlerPath(__dirname),
  events: [
    getTrigger(getUserContract, {
      method: 'delete', // ts will throw an error because 'delete' != 'get'
			authorizer: 'arn::aws...' // will also throw here because we did not 
																// define an authorizer in this contract
    }),
  ],
```

### getHandler ⚙️

```tsx
// handler.ts
import { getHandler } from '@swarmion/serverless-contracts';
import { getUserContract } from '@my-app/contracts/getUserContract';

const handler = getHandler(getUserContract)(async event => {
  event.pathParameters.userId; // will have type 'string'

  event.toto; // will fail type checking
  event.pathParameters.toto; // will also fail

  return { id: '123456', name: 'Doe' }; // also type-safe!
});

export default handler;
```

The `getHandler` helpers does multiple things:

- **Full typing** for input and output in IDE
- **Body parsing** and output serialization
- HTTP **errors handling**
- input and output **runtime validation** using ajv

## Consumer features

Once the endpoint is defined, we want to request it from another service (a **frontend application** for example).
To do so, you can use consumer helpers to generate type safe requests.

### getFetchRequest 🌐

```tsx
// useGetUser.ts
import { getFetchRequest } from '@swarmion/serverless-contracts';
import { getUserContract } from '@my-app/contracts/getUserContract';

await getFetchRequest(getUserContract, fetch, {
  pathParameters: { userId: '15' }, // correctly typed
  headers: {
    'my-header': 'hello', // will fail type checking
  },
  baseUrl: 'https://my-app.com',
});
```

---

## Conclusion ⭐

**Swarmion Serverless contracts helps you build scalable applications with multiple services and strong interfaces between them.** We presented you API Gateway contracts but we also recently implemented EventBridge contracts for message interfaces between services.

Also, Swarmion comes with more features, including a project and service generator which implements all the best practices we gathered at Kumo.

You can check our [developer guide here](https://www.swarmion.dev/), everything is open source, feel free to contact us and contribute!