Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): support generics #284

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

feat(types): support generics #284

wants to merge 18 commits into from

Conversation

naorpeled
Copy link
Collaborator

@naorpeled naorpeled commented Feb 6, 2025

Basic Type-Safe Setup

import { API, Request, Response, ALBContext, APIGatewayContext, APIGatewayV2Context } from 'lambda-api';
import { ALBEvent, APIGatewayProxyEvent, APIGatewayProxyEventV2 } from 'aws-lambda';

// Initialize with type inference
const api = new API();

Type-Safe Request Handlers

ALB Handler Example

interface UserData {
  id: string;
  name: string;
  email: string;
}

// Type-safe ALB request handler
api.get<UserData, ALBContext>('/users', (req, res) => {
  // req.requestContext is typed as ALBContext
  console.log(req.requestContext.elb.targetGroupArn);
  
  // Type-safe response
  res.json({
    id: '123',
    name: 'John Doe',
    email: '[email protected]'
  });
});

API Gateway v1 Handler Example

// Type-safe API Gateway v1 request handler
api.post<UserData, APIGatewayContext>('/users', (req, res) => {
  // req.requestContext is typed as APIGatewayContext
  console.log(req.requestContext.requestId);
  console.log(req.requestContext.identity.sourceIp);
  
  res.json({
    id: req.requestContext.requestId,
    name: req.body.name,
    email: req.body.email
  });
});

API Gateway v2 Handler Example

// Type-safe API Gateway v2 request handler
api.put<UserData, APIGatewayV2Context>('/users/:id', (req, res) => {
  // req.requestContext is typed as APIGatewayV2Context
  console.log(req.requestContext.http.sourceIp);
  
  res.json({
    id: req.params.id,
    name: req.body.name,
    email: req.body.email
  });
});

Type-Safe Middleware

Source-Agnostic Middleware

import { Middleware, isApiGatewayContext, isApiGatewayV2Context, isAlbContext } from 'lambda-api';

const sourceAgnosticMiddleware: Middleware = (req, res, next) => {
  // Type guards help narrow down the request context type
  if (isAlbContext(req.requestContext)) {
    // ALB specific logic
    console.log(req.requestContext.elb.targetGroupArn);
  } else if (isApiGatewayV2Context(req.requestContext)) {
    // API Gateway v2 specific logic
    console.log(req.requestContext.http.sourceIp);
  } else if (isApiGatewayContext(req.requestContext)) {
    // API Gateway v1 specific logic
    console.log(req.requestContext.identity.sourceIp);
  }
  
  next();
};

api.use(sourceAgnosticMiddleware);

Source-Specific Middleware

// ALB-specific middleware
const albMiddleware: Middleware<any, ALBContext> = (req, res, next) => {
  // req.requestContext is typed as ALBContext
  console.log(req.requestContext.elb.targetGroupArn);
  next();
};

// API Gateway v2 specific middleware
const apiGwV2Middleware: Middleware<any, APIGatewayV2Context> = (req, res, next) => {
  // req.requestContext is typed as APIGatewayV2Context
  console.log(req.requestContext.http.sourceIp);
  next();
};

Type-Safe Error Handling

import { ErrorHandlingMiddleware } from 'lambda-api';

const errorHandler: ErrorHandlingMiddleware = (error, req, res, next) => {
  if (isAlbContext(req.requestContext)) {
    // ALB specific error handling
    res.status(500).json({
      message: error.message,
      targetGroup: req.requestContext.elb.targetGroupArn
    });
  } else {
    // Default error handling
    res.status(500).json({
      message: error.message
    });
  }
};

api.use(errorHandler);

Advanced Type-Safe Examples

Custom Request Types

interface CustomQuery {
  filter?: string;
  page?: string;
}

interface CustomParams {
  userId: string;
}

interface CustomBody {
  name: string;
  email: string;
}

// Fully typed request handler
api.get<
  UserData,
  ALBContext,
  CustomQuery,
  CustomParams,
  CustomBody
>('/users/:userId', (req, res) => {
  // All properties are properly typed
  const { filter, page } = req.query;
  const { userId } = req.params;
  const { name, email } = req.body;
  
  res.json({
    id: userId,
    name,
    email
  });
});

Response Type Extensions

// Extend Response interface with custom methods
declare module 'lambda-api' {
  interface Response {
    sendWithTimestamp?: (data: any) => void;
  }
}

const responseEnhancer: Middleware = (req, res, next) => {
  res.sendWithTimestamp = (data: any) => {
    res.json({
      ...data,
      timestamp: Date.now()
    });
  };
  next();
};

api.use(responseEnhancer);

// Use custom response method
api.get('/users', (req, res) => {
  res.sendWithTimestamp({ name: 'John' });
});

Using Built-in Auth Property

interface AuthInfo {
  userId: string;
  roles: string[];
  type: 'Bearer' | 'Basic' | 'OAuth' | 'Digest' | 'none';
  value: string | null;
}

function hasAuth(req: Request): req is Request & { auth: AuthInfo } {
  return 'auth' in req && req.auth?.type !== undefined;
}

api.get('/protected', (req, res) => {
  if (hasAuth(req)) {
    // req.auth is now typed as AuthInfo
    const { userId, roles } = req.auth;
    res.json({ userId, roles });
  } else {
    res.status(401).json({ message: 'Unauthorized' });
  }
});

Running the API

// Type-safe run method
export const handler = async (
  event: ALBEvent | APIGatewayProxyEvent | APIGatewayProxyEventV2,
  context: any
) => {
  return api.run(event, context);
};

Type Guards Usage

import {
  isAlbContext,
  isAlbEvent,
  isAlbRequest,
  isApiGatewayContext,
  isApiGatewayEvent,
  isApiGatewayRequest,
  isApiGatewayV2Context,
  isApiGatewayV2Event,
  isApiGatewayV2Request
} from 'lambda-api';

api.use((req, res, next) => {
  // Event type guards
  if (isAlbEvent(req.app._event)) {
    // ALB specific logic
  }
  
  // Context type guards
  if (isAlbContext(req.requestContext)) {
    // ALB specific logic
  }
  
  // Request type guards
  if (isAlbRequest(req)) {
    // ALB specific logic
  }
  
  next();
});

Best Practices

  1. Always specify response types for better type inference:
interface ResponseType {
  message: string;
  code: number;
}

api.get<ResponseType>('/status', (req, res) => {
  res.json({
    message: 'OK',
    code: 200
  });
});
  1. Use type guards for source-specific logic:
api.use((req, res, next) => {
  if (isAlbContext(req.requestContext)) {
    // ALB-specific logging
    console.log(`ALB Request to ${req.requestContext.elb.targetGroupArn}`);
  }
  next();
});
  1. Leverage TypeScript's type inference with middleware:
const typedMiddleware: Middleware<ResponseType, ALBContext> = (req, res, next) => {
  // Full type information available
  next();
};
  1. Use source-specific error handling:
api.use((error, req, res, next) => {
  const baseError = {
    message: error.message,
    timestamp: new Date().toISOString()
  };

  if (isAlbContext(req.requestContext)) {
    res.status(500).json({
      ...baseError,
      targetGroup: req.requestContext.elb.targetGroupArn
    });
  } else if (isApiGatewayV2Context(req.requestContext)) {
    res.status(500).json({
      ...baseError,
      stage: req.requestContext.stage
    });
  } else {
    res.status(500).json(baseError);
  }
});

Issues

implements #276
and closes #244

@vandrade-git
Copy link
Contributor

This seems like a great change.

I've just tried on a project I have and noticed a few things. Given:

api.get<Response, ALBContext>('/health', async (req, res) => {
  console.log(req.requestContext.elb); <-- also type any, any, any

  res.json({ status: 'ok' });
});

both req and res have an implicit type any so the type checking does not seem to be doing what is described in the README.

src/index.ts:20:52 - error TS7006: Parameter 'req' implicitly has an 'any' type.
20 api.get<Response, ALBContext>('/health', async (req, res) => {

src/index.ts:20:57 - error TS7006: Parameter 'res' implicitly has an 'any' type.
20 api.get<Response, ALBContext>('/health', async (req, res) => {

This seems to be a limitation of the type

...
(
  | Middleware<TResponse, TContext, TQuery, TParams, TBody>
  | HandlerFunction<TResponse, TContext, TQuery, TParams, TBody>
)
...

Something like:

const health: HandlerFunction<object, ALBContext> = async (req, res) => {
  console.log(req.requestContext.elb);

  res.json({ status: 'ok' });
};
// public health endpoint
api.get<Response, ALBContext>('/health', health);

seems to over fine but it is a bit more cumbersome.

@naorpeled
Copy link
Collaborator Author

naorpeled commented Feb 9, 2025

This seems like a great change.

I've just tried on a project I have and noticed a few things. Given:

api.get<Response, ALBContext>('/health', async (req, res) => {
  console.log(req.requestContext.elb); <-- also type any, any, any

  res.json({ status: 'ok' });
});

both req and res have an implicit type any so the type checking does not seem to be doing what is described in the README.

src/index.ts:20:52 - error TS7006: Parameter 'req' implicitly has an 'any' type.
20 api.get<Response, ALBContext>('/health', async (req, res) => {

src/index.ts:20:57 - error TS7006: Parameter 'res' implicitly has an 'any' type.
20 api.get<Response, ALBContext>('/health', async (req, res) => {

This seems to be a limitation of the type

...
(
  | Middleware<TResponse, TContext, TQuery, TParams, TBody>
  | HandlerFunction<TResponse, TContext, TQuery, TParams, TBody>
)
...

Something like:

const health: HandlerFunction<object, ALBContext> = async (req, res) => {
  console.log(req.requestContext.elb);

  res.json({ status: 'ok' });
};
// public health endpoint
api.get<Response, ALBContext>('/health', health);

seems to over fine but it is a bit more cumbersome.

I see, I'll dig into it in the next few days.
Thanks for the feedback, very appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature request] Typescript - Support generic request / response types
2 participants