├── .gitignore ├── .vscode └── settings.json ├── Makefile ├── README.md └── api-stack ├── .gitignore ├── README.md ├── lib ├── MyStack.ts └── index.ts ├── package.json ├── src ├── api.py └── requirements.txt ├── sst.json ├── test └── MyStack.test.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | fastapi-app 4 | 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.venvFolders": [ 3 | "${workspaceFolder}/fastapi-app" 4 | ], 5 | "python.analysis.autoImportCompletions": true, 6 | "python.analysis.extraPaths": [ 7 | "path/to/root/dir/" 8 | ], 9 | "python.pythonPath": "fastapi-app/bin/python3.8", 10 | "cSpell.words": [ 11 | "pydantic" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | requirements: 2 | pip3 freeze > $(shell pwd)/fast-api-stack/src/requirements.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fast API Serverless Stack 2 | 3 | Building type safe APIs in AWS lambda with a local development experience 4 | 5 | ## Tools 6 | 7 | - Fast API for api handlers 8 | - Mangum for api gateway compatibility layers 9 | - Pydantic to create inline schema validation 10 | 11 | ## Benefits 12 | 13 | - Fast development speed 14 | - Run locally using serverless stack 15 | - Publishable documentation for the API out of the box 16 | 17 | ## Background Reading 18 | 19 | - https://dev.to/tiangolo/the-future-of-fastapi-and-pydantic-is-bright-3pbm 20 | - https://docs.serverless-stack.com/constructs/Api 21 | - https://iwpnd.pw/articles/2020-01/deploy-fastapi-to-aws-lambda 22 | -------------------------------------------------------------------------------- /api-stack/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # typescript 13 | *.js 14 | *.d.ts 15 | 16 | # misc 17 | .DS_Store 18 | 19 | # sst build output 20 | .build 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | -------------------------------------------------------------------------------- /api-stack/README.md: -------------------------------------------------------------------------------- 1 | # API Stack 2 | 3 | Deploys the API and ensures that the API routes are private to the cognito JWT authorizer 4 | -------------------------------------------------------------------------------- /api-stack/lib/MyStack.ts: -------------------------------------------------------------------------------- 1 | import * as sst from "@serverless-stack/resources"; 2 | import { HttpUserPoolAuthorizer } from '@aws-cdk/aws-apigatewayv2-authorizers' 3 | import { ApiAuthorizationType } from "@serverless-stack/resources"; 4 | 5 | export default class MyStack extends sst.Stack { 6 | constructor(scope: sst.App, id: string, props?: sst.StackProps) { 7 | super(scope, id, props); 8 | 9 | const { cognitoUserPool, cognitoUserPoolClient } = new sst.Auth(this, "Auth", { 10 | cognito: true, 11 | }); 12 | 13 | const authorizer = cognitoUserPool && cognitoUserPoolClient && 14 | new HttpUserPoolAuthorizer({ 15 | userPool: cognitoUserPool, 16 | userPoolClient: cognitoUserPoolClient, 17 | }) 18 | 19 | // Create a HTTP API 20 | const api = new sst.Api(this, "Api", { 21 | defaultFunctionProps: { 22 | srcPath: "src", 23 | runtime: "python3.8" 24 | }, 25 | routes: { 26 | "ANY /{proxy+}": { 27 | function: { 28 | handler: "api.handler", 29 | }, 30 | authorizationType: ApiAuthorizationType.JWT, 31 | authorizer 32 | }, 33 | "GET /docs": "api.handler", 34 | "GET /openapi.json": "api.handler" 35 | } 36 | }); 37 | 38 | this.addOutputs({ 39 | "ApiEndpoint": api.url, 40 | "CognitoUserPoll": cognitoUserPool?.userPoolId || "", 41 | "CognitoAppClient": cognitoUserPoolClient?.userPoolClientId || "" 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api-stack/lib/index.ts: -------------------------------------------------------------------------------- 1 | import MyStack from "./MyStack"; 2 | import * as sst from "@serverless-stack/resources"; 3 | 4 | export default function main(app: sst.App): void { 5 | new MyStack(app, "my-stack"); 6 | } 7 | -------------------------------------------------------------------------------- /api-stack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-stack", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "test": "sst test", 7 | "start": "sst start", 8 | "build": "sst build", 9 | "deploy": "sst deploy", 10 | "remove": "sst remove" 11 | }, 12 | "devDependencies": { 13 | "@aws-cdk/assert": "1.105.0", 14 | "@types/aws-lambda": "^8.10.70" 15 | }, 16 | "dependencies": { 17 | "@serverless-stack/cli": "0.29.2", 18 | "@serverless-stack/resources": "0.29.2", 19 | "@aws-cdk/core": "1.105.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api-stack/src/api.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from mangum import Mangum 4 | from fastapi import FastAPI 5 | 6 | from pydantic import BaseModel, validator 7 | from datetime import date, timedelta 8 | 9 | 10 | class Item(BaseModel): 11 | name: str 12 | description: Optional[str] = None 13 | price: float 14 | date: date 15 | 16 | @validator('date') 17 | def check_date_is_valid(cls, v): 18 | if v > date.today(): 19 | raise ValueError('Date must be in the past.') 20 | if v < date.today() - timedelta(days=120): 21 | raise ValueError('Date must be within 120 days') 22 | return v 23 | 24 | 25 | APP = FastAPI( 26 | title="Example Test API", 27 | description="Describe API documentation to be served; types come from " 28 | "pydantic, routes from the decorators, and docs from the fastapi internal", 29 | version="0.0.1", 30 | ) 31 | 32 | 33 | @APP.get("/v1/items", response_model=List[Item]) 34 | def list_items(): 35 | """ 36 | Return a collection of items 37 | """ 38 | return [ 39 | Item( 40 | name="Purple Jumper", 41 | price=10.99, 42 | date=date.today() 43 | ), 44 | Item( 45 | name="Red Jumper", 46 | price=11.99, 47 | date=date.today() - timedelta(days=10) 48 | ) 49 | ] 50 | 51 | 52 | @APP.post("/v1/items", response_model=Item) 53 | def create_item(item: Item): 54 | """ 55 | Create a new item 56 | """ 57 | return item 58 | 59 | 60 | handler = Mangum(APP, lifespan="off") 61 | -------------------------------------------------------------------------------- /api-stack/src/requirements.txt: -------------------------------------------------------------------------------- 1 | autopep8==1.5.7 2 | fastapi==0.65.2 3 | mangum==0.11.0 4 | pycodestyle==2.7.0 5 | pydantic==1.8.2 6 | six==1.16.0 7 | starlette==0.14.2 8 | toml==0.10.2 9 | typing-extensions==3.10.0.0 10 | -------------------------------------------------------------------------------- /api-stack/sst.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-stack", 3 | "stage": "dev", 4 | "region": "us-east-1", 5 | "lint": true, 6 | "typeCheck": true 7 | } 8 | -------------------------------------------------------------------------------- /api-stack/test/MyStack.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, haveResource } from "@aws-cdk/assert"; 2 | import * as sst from "@serverless-stack/resources"; 3 | import MyStack from "../lib/MyStack"; 4 | 5 | test("Test Stack", () => { 6 | const app = new sst.App(); 7 | // WHEN 8 | const stack = new MyStack(app, "test-stack"); 9 | // THEN 10 | expect(stack).to(haveResource("AWS::Lambda::Function")); 11 | }); 12 | -------------------------------------------------------------------------------- /api-stack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "include": ["lib","src"] 23 | } 24 | --------------------------------------------------------------------------------