npm package: @phila/philaroute
Description: Basic HTTP router for AWS API Gateway / Proxy Lambdas with composable pipelines built with TypeScript.
Author: Darren McDowell
Latest version: 1.0.14

# philaroute

[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org/)
[![Hosted on AWS](https://img.shields.io/badge/hosted%20on-AWS-orange?logo=amazon-aws)](https://aws.amazon.com/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/CityOfPhiladelphia/philaroute/blob/main/LICENSE)

[![Build Status](https://github.com/CityOfPhiladelphia/philaroute/actions/workflows/publish.yml/badge.svg)](https://github.com/CityOfPhiladelphia/philaroute/actions/workflows/publish.yml)
[![Test Status](https://github.com/CityOfPhiladelphia/philaroute/actions/workflows/test.yml/badge.svg)](https://github.com/CityOfPhiladelphia/philaroute/actions/workflows/test.yml)

A lightweight, composable HTTP router for Node.js Lambda/API Gateway environments, written in TypeScript. Supports composable middleware pipelines and CORS out of the box.

---

## Features

- TypeScript-first API with full type definitions
- Simple route and method registration
- Functional middleware pipeline composition
- Built-in input validation (environment, query params, request body)
- Built-in CORS support for Lambda/API Gateway
- Automatic error handling with proper HTTP status codes

---

## Installation

```bash
npm install philaroute
```

---

## Quick Start

```typescript
import { Router, http } from 'philaroute';

const router = Router({ cors: { 'Access-Control-Allow-Origin': '*' } });
const hello = router.path('/hello');

hello.get([
  async (acc) => {
    acc.data.message = 'Hello World!';
    return acc;
  }
]);

// Lambda handler
export const main = async (event) => {
  return router.routeToPath(event);
};
```

---

## Core Concepts

### The Accumulator Pattern

All request data flows through a `RestAccumulator` object that gets passed through your pipeline functions:

```typescript
type RestAccumulator<TData = Record<string, any>> = {
  request?: {
    params?: { [key: string]: string },  // Query string parameters
    body?: any,                           // Parsed JSON request body
    headers?: { [key: string]: string },  // HTTP headers
    mvHeaders?: { [key: string]: string[] }, // Multi-value headers
  },
  data: TData,                            // Pipeline state (see note below)
  response: {
    statusCode?: number,
    headers?: { [key: string]: string },
    isBase64Encoded?: boolean,
    body?: string | Record<string, any> | any[]
  }
}
```

**Important:** If `response.body` is not explicitly set, the `data` object becomes the response body automatically. This allows pipelines to build up response data without explicitly setting the body.

### Pipeline Functions

Each function in a pipeline receives the accumulator and returns a modified version:

```typescript
type PipelineFn = (acc: RestAccumulator) => Promise<RestAccumulator>
```

Functions execute sequentially. Each receives the output of the previous function.

### Type-Safe Pipelines

For full compile-time type safety, use `typedPipeline` which tracks the accumulator shape through each transformation:

```typescript
import { typedPipeline, TransformFn } from 'philaroute';

type Input = { value: number };
type Output = { value: number; doubled: number };

const double: TransformFn<Input, Output> = async (acc) => ({
  ...acc,
  doubled: acc.value * 2
});

const pipeline = typedPipeline(double);
const result = await pipeline({ value: 5 });
// result.doubled is typed as number
```

The `typedPipeline` uses TypeScript's tuple inference to validate that each function's output type matches the next function's input type at compile time.

---

## API Reference

### Router

Create a router instance with optional CORS configuration:

```typescript
import { Router } from 'philaroute';

const router = Router({
  cors: {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
  }
});
```

### Registering Routes

```typescript
const users = router.path('/users');

users.get([...pipeline]);    // GET /users
users.post([...pipeline]);   // POST /users
users.put([...pipeline]);    // PUT /users
users.patch([...pipeline]);  // PATCH /users
users.delete([...pipeline]); // DELETE /users
```

OPTIONS requests are handled automatically with a 204 No Content response.

### Dispatching Requests

```typescript
// In your Lambda handler
export const handler = async (event) => {
  return router.routeToPath(event);
};
```

---

## Validation

Built-in validators throw `AssertionError` on failure, which the router converts to 400 Bad Request responses. All validators provide compile-time type inference for the validated fields.

### Environment Variables

```typescript
import { validate } from 'philaroute';

users.get([
  validate.environment(['DATABASE_URL', 'API_KEY']),
  async (acc) => {
    // TypeScript knows these are strings:
    const dbUrl: string = acc.data.valid.environment.DATABASE_URL;
    const apiKey: string = acc.data.valid.environment.API_KEY;
    return acc;
  }
]);
```

### Query Parameters

```typescript
users.get([
  validate.parameters(['page', 'limit']),
  async (acc) => {
    // TypeScript knows these are strings:
    const page: string = acc.data.valid.parameters.page;
    const limit: string = acc.data.valid.parameters.limit;
    return acc;
  }
]);
```

### Request Body

Validate body fields with type checking. The schema maps to TypeScript types:

```typescript
users.post([
  validate.body({
    name: 'string',
    age: 'number',
    active: 'boolean',
    metadata: 'object',
    tags: 'array'
  }),
  async (acc) => {
    // TypeScript infers the correct types:
    const name: string = acc.data.valid.body.name;
    const age: number = acc.data.valid.body.age;
    const active: boolean = acc.data.valid.body.active;
    return acc;
  }
]);
```

---

## Response Handling

### Explicit Response

Use `http.response()` to set response properties:

```typescript
import { http } from 'philaroute';

users.post([
  validate.body({ name: 'string' }),
  async (acc) => {
    const user = await createUser(acc.data.valid.body);
    acc.data.user = user;
    return acc;
  },
  http.response({
    statusCode: 201,
    body: { success: true }
  })
]);
```

### Implicit Response

If you don't set `response.body`, the `data` object is returned:

```typescript
users.get([
  async (acc) => {
    acc.data.users = await fetchUsers();
    acc.data.total = 100;
    return acc;
  }
  // Response body will be: { users: [...], total: 100 }
]);
```

---

## Error Handling

The router automatically handles errors:

| Error Type | HTTP Status |
|-----------|-------------|
| Path not found | 404 Not Found |
| Method not allowed | 405 Method Not Allowed |
| AssertionError (validation) | 400 Bad Request |
| Any other error | 500 Internal Server Error |

CORS headers are included in all error responses.

---

## Complete Example

```typescript
import { Router, validate, http } from 'philaroute';

const router = Router({
  cors: { 'Access-Control-Allow-Origin': '*' }
});

const users = router.path('/users');

users.get([
  validate.parameters(['page']),
  async (acc) => {
    const page = parseInt(acc.data.valid.parameters.page, 10);
    acc.data.users = await db.users.findMany({ skip: page * 10, take: 10 });
    return acc;
  }
]);

users.post([
  validate.body({ name: 'string', email: 'string' }),
  async (acc) => {
    const user = await db.users.create(acc.data.valid.body);
    acc.data.user = user;
    return acc;
  },
  http.response({ statusCode: 201 })
]);

export const handler = (event) => router.routeToPath(event);
```

---

## Module Structure

| Module | Purpose |
|--------|---------|
| `index.ts` | Router factory, route registration, request dispatch |
| `fn.ts` | Pipeline composition (`pipeline`, `typedPipeline`, `restPipeline`) |
| `parse.ts` | AWS event to RestAccumulator transformation |
| `http.ts` | Response formatting and HTTP status codes |
| `validate.ts` | Input validation middleware with type inference |
| `types.ts` | Core TypeScript type definitions |
| `aws.ts` | AWS API Gateway v1 payload interface |

---

## Development

```bash
npm run build   # Build TypeScript
npm test        # Run tests
npm run lint    # Lint code
npm run format  # Format code
```

---

## License

MIT © City of Philadelphia