├── .npmrc ├── pnpm-workspace.yaml ├── packages ├── helpers │ ├── index.ts │ ├── response.ts │ ├── tests │ │ └── response.test.ts │ ├── package.json │ └── middify.ts ├── tsconfig │ ├── README.md │ ├── package.json │ └── base.json └── eslint-config-custom │ ├── package.json │ └── eslint.config.mjs ├── .vscode └── settings.json ├── eslint.config.js ├── services ├── api1 │ ├── eslint.config.ts │ ├── tsconfig.json │ ├── src │ │ └── functions │ │ │ ├── hello │ │ │ ├── schema.json │ │ │ ├── schema.ts │ │ │ └── handler.ts │ │ │ └── index.yml │ ├── README.md │ ├── serverless.yml │ └── package.json └── api2 │ ├── eslint.config.ts │ ├── src │ └── functions │ │ ├── ciao │ │ ├── schema.ts │ │ ├── schema.json │ │ └── handler.ts │ │ └── index.yml │ ├── tsconfig.json │ ├── README.md │ ├── serverless.yml │ └── package.json ├── .gitignore ├── turbo.json ├── .github └── workflows │ └── ci.yml ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-manager-strict=false 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "services/*" 4 | -------------------------------------------------------------------------------- /packages/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./middify"; 2 | export * from "./response"; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | "./services/api1", 4 | "./services/api2", 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import config from 'eslint-config-custom/eslint.config.mjs'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /services/api1/eslint.config.ts: -------------------------------------------------------------------------------- 1 | import config from "eslint-config-custom/eslint.config.mjs"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /services/api2/eslint.config.ts: -------------------------------------------------------------------------------- 1 | import config from "eslint-config-custom/eslint.config.mjs"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/helpers/response.ts: -------------------------------------------------------------------------------- 1 | export const formatJSONResponse = (response: Record) => { 2 | return { 3 | statusCode: 200, 4 | body: JSON.stringify(response), 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /services/api2/src/functions/ciao/schema.ts: -------------------------------------------------------------------------------- 1 | import bodySchema from "./schema.json"; 2 | 3 | export const schema = { 4 | type: "object", 5 | required: ["body"], 6 | properties: { 7 | body: bodySchema, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /services/api1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@functions/*": ["src/functions/*"], 7 | } 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /services/api2/src/functions/ciao/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string" 7 | } 8 | }, 9 | "required": ["name"], 10 | "additionalProperties": false 11 | } -------------------------------------------------------------------------------- /services/api2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@functions/*": ["src/functions/*"], 7 | } 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /services/api1/src/functions/hello/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "properties": { 5 | "name": { 6 | "type": "string" 7 | } 8 | }, 9 | "required": ["name"], 10 | "additionalProperties": false 11 | } -------------------------------------------------------------------------------- /services/api2/src/functions/index.yml: -------------------------------------------------------------------------------- 1 | ciao: 2 | handler: src/functions/ciao/handler.main 3 | description: Lambda function to say ciao 4 | memorySize: 256 5 | events: 6 | - http: 7 | method: post 8 | path: ciao 9 | cors: true 10 | request: 11 | schemas: 12 | application/json: ${file(src/functions/ciao/schema.json)} 13 | -------------------------------------------------------------------------------- /services/api1/src/functions/index.yml: -------------------------------------------------------------------------------- 1 | hello: 2 | handler: src/functions/hello/handler.main 3 | description: Lambda function to say hello 4 | memorySize: 256 5 | events: 6 | - http: 7 | method: post 8 | path: hello 9 | cors: true 10 | request: 11 | schemas: 12 | application/json: ${file(src/functions/hello/schema.json)} 13 | -------------------------------------------------------------------------------- /services/api1/src/functions/hello/schema.ts: -------------------------------------------------------------------------------- 1 | import bodySchema from "./schema.json"; 2 | 3 | export const schema = { 4 | type: "object", 5 | required: ["body"], 6 | properties: { 7 | body: bodySchema, 8 | // possible to add pathParameters and queryStringParameters validation 9 | // pathParameters: pathSchema, 10 | // queryStringParameters: queryStringSchema 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": [ 7 | "base.json" 8 | ], 9 | "scripts": { 10 | "check:updates": "npx npm-check-updates", 11 | "check:updates:u": "npx npm-check-updates -u && pnpm i" 12 | }, 13 | "devDependencies": { 14 | "tsconfig-paths": "^4.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/helpers/tests/response.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | 3 | import { formatJSONResponse } from "../response"; 4 | 5 | describe("formatJSONResponse", () => { 6 | it("should return status 200", () => { 7 | const result = formatJSONResponse({ message: "Hello world" }); 8 | expect(result.statusCode).toEqual(200); 9 | expect(result.body).toEqual('{"message":"Hello world"}'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /services/api2/README.md: -------------------------------------------------------------------------------- 1 | # API 2 2 | 3 | Backend service using [serverless framework](https://www.serverless.com/). 4 | 5 | ## Run this service 6 | 7 | - install the dependencies: `pnpm i`. 8 | - build and run the services: `pnpm dev`. 9 | - send first request: 10 | 11 | ```bash 12 | curl -X POST http://localhost:4000/dev/ciao -H "Content-Type: application/json" -d '{"name": "world"}' 13 | ``` 14 | 15 | ```json 16 | { "message": "Ciao World !" } 17 | ``` -------------------------------------------------------------------------------- /services/api1/README.md: -------------------------------------------------------------------------------- 1 | # API 1 2 | 3 | Backend service using [serverless framework](https://www.serverless.com/). 4 | 5 | ## Run this service 6 | 7 | - install the dependencies: `pnpm i`. 8 | - build and run the services: `pnpm dev`. 9 | - send first request: 10 | 11 | ```bash 12 | curl -X POST http://localhost:3000/dev/hello -H "Content-Type: application/json" -d '{"name": "world"}' 13 | ``` 14 | 15 | ```json 16 | { "message": "Hello World !" } 17 | ``` 18 | -------------------------------------------------------------------------------- /services/api1/src/functions/hello/handler.ts: -------------------------------------------------------------------------------- 1 | import type { CustomHandler } from "helpers"; 2 | import { middyfy, formatJSONResponse } from "helpers"; 3 | 4 | import { bodySchema, schema } from "./schema"; 5 | 6 | const hello: CustomHandler = async (event) => { 7 | await new Promise((res) => setTimeout(res, 500)); 8 | 9 | return formatJSONResponse({ 10 | message: `Hello ${event.body.name} !`, 11 | }); 12 | }; 13 | 14 | export const main = middyfy(hello, schema); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # misc 12 | .DS_Store 13 | *.pem 14 | 15 | # debug 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | .pnpm-debug.log* 20 | 21 | # local env files 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | # turbo 28 | .turbo 29 | 30 | # serverless 31 | .build 32 | .serverless 33 | .esbuild 34 | -------------------------------------------------------------------------------- /services/api2/src/functions/ciao/handler.ts: -------------------------------------------------------------------------------- 1 | import type { CustomHandler } from "helpers"; 2 | import { middyfy, formatJSONResponse } from "helpers"; 3 | 4 | import { bodySchema, schema } from "./schema"; 5 | import capitalize from "lodash.capitalize"; 6 | 7 | const ciao: CustomHandler = async (event) => { 8 | await new Promise((res) => setTimeout(res, 500)); 9 | 10 | return formatJSONResponse({ 11 | message: `Ciao ${capitalize(event.body.name)} !`, 12 | }); 13 | }; 14 | 15 | export const main = middyfy(ciao, schema); 16 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "check:updates": "npx npm-check-updates", 7 | "check:updates:u": "npx npm-check-updates -u && pnpm i" 8 | }, 9 | "devDependencies": { 10 | "@eslint/js": "^9.20.0", 11 | "eslint": "^9.20.1", 12 | "eslint-config-prettier": "^10.0.1", 13 | "eslint-plugin-node": "^11.1.0", 14 | "eslint-plugin-vitest": "^0.5.4", 15 | "globals": "^15.15.0", 16 | "jiti": "^2.4.2", 17 | "typescript": "^5.7.3", 18 | "typescript-eslint": "^8.24.1" 19 | }, 20 | "publishConfig": { 21 | "access": "public" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | import vitest from "eslint-plugin-vitest"; 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | {files: ["**/*.{js,mjs,cjs,ts}"]}, 9 | {languageOptions: { globals: globals.browser }}, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | { 13 | files: ["tests/**"], 14 | plugins: { 15 | vitest, 16 | }, 17 | rules: { 18 | ...vitest.configs.recommended.rules, 19 | }, 20 | languageOptions: { 21 | globals: { 22 | ...vitest.environments.env.globals, 23 | }, 24 | }, 25 | }, 26 | ]; -------------------------------------------------------------------------------- /packages/helpers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helpers", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "index.ts", 6 | "types": "index.ts", 7 | "scripts": { 8 | "check:updates": "npx npm-check-updates", 9 | "check:updates:u": "npx npm-check-updates -u && pnpm i", 10 | "test": "vitest run --coverage", 11 | "test:watch": "vitest" 12 | }, 13 | "dependencies": { 14 | "@middy/core": "^6.0.0", 15 | "@middy/http-cors": "^6.0.0", 16 | "@middy/http-error-handler": "^6.0.0", 17 | "@middy/http-event-normalizer": "^6.0.0", 18 | "@middy/http-json-body-parser": "^6.0.0", 19 | "@middy/util": "^6.0.0", 20 | "@middy/validator": "^6.0.0" 21 | }, 22 | "devDependencies": { 23 | "@types/aws-lambda": "^8.10.147", 24 | "json-schema-to-ts": "^3.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /services/api1/serverless.yml: -------------------------------------------------------------------------------- 1 | service: api2 2 | frameworkVersion: "3.38.0" 3 | plugins: 4 | - serverless-esbuild 5 | - serverless-offline 6 | 7 | provider: 8 | name: aws 9 | runtime: nodejs20.x 10 | stage: dev 11 | region: eu-central-1 12 | environment: 13 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" 14 | NODE_OPTIONS: "--enable-source-maps --stack-trace-limit=1000" 15 | deploymentMethod: direct 16 | architecture: arm64 17 | 18 | functions: 19 | - ${file(src/functions/index.yml)} 20 | 21 | package: 22 | individually: true 23 | 24 | custom: 25 | esbuild: 26 | bundle: true 27 | minify: false 28 | sourcemap: true 29 | exclude: 30 | - aws-sdk 31 | target: node22 32 | define: 33 | require.resolve: undefined 34 | platform: node 35 | concurrency: 10 36 | serverless-offline: 37 | httpPort: 3000 38 | lambdaPort: 3002 39 | -------------------------------------------------------------------------------- /services/api2/serverless.yml: -------------------------------------------------------------------------------- 1 | service: api2 2 | frameworkVersion: "3.38.0" 3 | plugins: 4 | - serverless-esbuild 5 | - serverless-offline 6 | 7 | provider: 8 | name: aws 9 | runtime: nodejs20.x 10 | stage: dev 11 | region: eu-central-1 12 | environment: 13 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" 14 | NODE_OPTIONS: "--enable-source-maps --stack-trace-limit=1000" 15 | deploymentMethod: direct 16 | architecture: arm64 17 | 18 | functions: 19 | - ${file(src/functions/index.yml)} 20 | 21 | package: 22 | individually: true 23 | 24 | custom: 25 | esbuild: 26 | bundle: true 27 | minify: false 28 | sourcemap: true 29 | exclude: 30 | - aws-sdk 31 | target: node22 32 | define: 33 | require.resolve: undefined 34 | platform: node 35 | concurrency: 10 36 | serverless-offline: 37 | httpPort: 4000 38 | lambdaPort: 4002 39 | -------------------------------------------------------------------------------- /services/api1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api1", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "commonjs", 6 | "scripts": { 7 | "dev": "serverless offline start", 8 | "check:updates": "npx npm-check-updates", 9 | "check:updates:u": "npx npm-check-updates -u && pnpm i", 10 | "deploy:staging": "serverless deploy --aws-profile $AWS_PROFILE --stage staging", 11 | "deploy:production": "serverless deploy --aws-profile $AWS_PROFILE --stage production", 12 | "test": "vitest run --coverage --passWithNoTests", 13 | "test:watch": "vitest --passWithNoTests", 14 | "lint": "eslint '**/*.ts'", 15 | "lint:fix": "eslint '**/*.ts' --fix", 16 | "package": "serverless package" 17 | }, 18 | "devDependencies": { 19 | "eslint-config-custom": "workspace:*", 20 | "tsconfig": "workspace:*" 21 | }, 22 | "dependencies": { 23 | "helpers": "workspace:*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es2021", 5 | "lib": ["es2021"], 6 | "outDir": "./dist", 7 | "module": "commonjs", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "skipLibCheck": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "removeComments": true, 21 | "resolveJsonModule": true, 22 | "declaration": false, 23 | "strictNullChecks": true 24 | }, 25 | "include": ["./src"], 26 | "exclude": ["./dist"], 27 | "ts-node": { 28 | "require": ["tsconfig-paths/register"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /services/api2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api2", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "commonjs", 6 | "scripts": { 7 | "dev": "serverless offline start", 8 | "check:updates": "npx npm-check-updates", 9 | "check:updates:u": "npx npm-check-updates -u && pnpm i", 10 | "deploy:staging": "serverless deploy --aws-profile $AWS_PROFILE --stage staging", 11 | "deploy:production": "serverless deploy --aws-profile $AWS_PROFILE --stage production", 12 | "test": "vitest run --coverage --passWithNoTests", 13 | "test:watch": "vitest --passWithNoTests", 14 | "lint": "eslint '**/*.ts'", 15 | "lint:fix": "eslint '**/*.ts' --fix", 16 | "package": "serverless package" 17 | }, 18 | "dependencies": { 19 | "helpers": "workspace:*", 20 | "lodash.capitalize": "^4.2.1" 21 | }, 22 | "devDependencies": { 23 | "@types/lodash.capitalize": "^4.2.9", 24 | "eslint-config-custom": "workspace:*", 25 | "tsconfig": "workspace:*" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "outputs": [ 9 | "dist/**" 10 | ] 11 | }, 12 | "test": { 13 | "dependsOn": [ 14 | "build" 15 | ], 16 | "inputs": [ 17 | "src/**/*.ts", 18 | "test/**/*.ts" 19 | ] 20 | }, 21 | "test:watch": {}, 22 | "lint": {}, 23 | "lint:fix": {}, 24 | "check:updates": {}, 25 | "check:updates:u": {}, 26 | "dev": { 27 | "cache": false 28 | }, 29 | "package": { 30 | "dependsOn": [ 31 | "build", 32 | "test", 33 | "lint" 34 | ], 35 | "cache": false 36 | }, 37 | "deploy:staging": { 38 | "dependsOn": [ 39 | "package" 40 | ], 41 | "cache": false 42 | }, 43 | "deploy:production": { 44 | "dependsOn": [ 45 | "package" 46 | ], 47 | "cache": false 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | timeout-minutes: 15 13 | runs-on: ${{ matrix.os }} 14 | # To use Remote Caching, uncomment the next lines and follow the steps below. 15 | # env: 16 | # TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 17 | # TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest] 21 | 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 2 27 | 28 | - uses: pnpm/action-setup@v3 29 | with: 30 | version: 9 31 | 32 | - name: Setup Node.js environment 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 22 36 | cache: "pnpm" 37 | 38 | - name: Install dependencies 39 | run: pnpm install 40 | 41 | - name: Build 42 | run: pnpm build 43 | 44 | - name: Test 45 | run: pnpm test 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turboless", 3 | "version": "1.1.2", 4 | "private": true, 5 | "type": "commonjs", 6 | "workspaces": [ 7 | "packages/*", 8 | "services/*" 9 | ], 10 | "scripts": { 11 | "build": "turbo run build", 12 | "dev": "turbo run dev --parallel", 13 | "deploy:staging": "turbo run deploy:staging --parallel", 14 | "deploy:production": "turbo run deploy:production --parallel", 15 | "lint": "turbo run lint", 16 | "lint:fix": "turbo run lint:fix", 17 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 18 | "test": "turbo run test", 19 | "test:watch": "turbo run test:watch", 20 | "check:updates": "turbo run check:updates", 21 | "check:updates:u": "turbo run check:updates:u", 22 | "package": "turbo run package" 23 | }, 24 | "devDependencies": { 25 | "@types/aws-lambda": "^8.10.147", 26 | "@types/node": "^22.13.4", 27 | "@vitest/coverage-v8": "^3.0.6", 28 | "esbuild": "^0.25.0", 29 | "eslint-config-custom": "workspace:*", 30 | "prettier": "latest", 31 | "serverless": "^3.38.0", 32 | "serverless-esbuild": "^1.54.6", 33 | "serverless-offline": "^14.4.0", 34 | "ts-node": "^10.9.2", 35 | "turbo": "^2.4.2", 36 | "typescript": "^5.7.3", 37 | "vitest": "^3.0.6" 38 | }, 39 | "engines": { 40 | "node": ">=22", 41 | "pnpm": ">=9", 42 | "npm": "please-use-pnpm", 43 | "yarn": "please-use-pnpm" 44 | }, 45 | "packageManager": "pnpm@10.4.1" 46 | } 47 | -------------------------------------------------------------------------------- /packages/helpers/middify.ts: -------------------------------------------------------------------------------- 1 | import middy, { MiddyfiedHandler } from "@middy/core"; 2 | import middyJsonBodyParser from "@middy/http-json-body-parser"; 3 | import cors from "@middy/http-cors"; 4 | import validator from "@middy/validator"; 5 | import { transpileSchema } from "@middy/validator/transpile"; 6 | import httpErrorHandler from "@middy/http-error-handler"; 7 | import httpEventNormalizer from "@middy/http-event-normalizer"; 8 | 9 | import type { 10 | APIGatewayProxyEvent as ProxyEvent, 11 | APIGatewayProxyResult, 12 | Context, 13 | Handler as AWSHandler, 14 | } from "aws-lambda"; 15 | import type { FromSchema } from "json-schema-to-ts"; 16 | 17 | // Taken from https://github.com/middyjs/middy/issues/316#issuecomment-1013351805 18 | // Event is an APIGatewayProxyEvent with a typed body, pathParameters and queryStringParameters which depends on http-json-body-parser & json-schema-to-ts 19 | // queryStringParameters and multiValueQueryStringParameters is non-nullable as we use http-event-normalizer 20 | export interface Event< 21 | TBody extends Record | null, 22 | TPathParameters extends Record | null, 23 | TQueryStringParameters extends Record | null 24 | > extends Omit< 25 | ProxyEvent, 26 | "body" | "pathParameters" | "queryStringParameters" 27 | > { 28 | body: TBody extends Record ? FromSchema : null; 29 | pathParameters: TPathParameters extends Record 30 | ? FromSchema 31 | : null; 32 | queryStringParameters: TQueryStringParameters extends Record 33 | ? FromSchema 34 | : null; 35 | multiValueQueryStringParameters: NonNullable< 36 | ProxyEvent["multiValueQueryStringParameters"] 37 | >; 38 | } 39 | 40 | // We are making use of http-response-serializer 41 | interface Result extends Omit { 42 | body: string | Record; 43 | } 44 | 45 | // Handler type which gives us proper types on our event based on TBody and TPathParameters which are JSON schemas 46 | export type CustomHandler< 47 | TBody extends Record | null = null, 48 | TPathParameters extends Record | null = null, 49 | TQueryStringParameters extends Record | null = null 50 | > = AWSHandler, Result>; 51 | 52 | // TODO: find a way to remove the couple of `any` here. Validation and typing are still working though 53 | export const middyfy = ( 54 | handler: any, 55 | schema: Record | null = null 56 | ): MiddyfiedHandler, Result, Error, Context> => { 57 | const wrapper: any = middy(handler) 58 | .use(middyJsonBodyParser()) 59 | .use(httpEventNormalizer()); 60 | 61 | if (schema) { 62 | wrapper.use(validator({ eventSchema: transpileSchema(schema) })); 63 | } 64 | 65 | return wrapper.use(cors()).use(httpErrorHandler({})); 66 | }; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Turboless 🐠 2 | 3 | This is an unofficial starter turborepo for serverless + typescript. 4 | 5 | Don't be fooled by it's name, it's simply a contraction of `turborepo` and `serverless`. 6 | 7 | ## What's inside? 8 | 9 | This turborepo uses [pnpm](https://pnpm.io) as a packages manager. It includes the following packages/apps: 10 | 11 | ### Packages 12 | 13 | - `eslint-config-custom`: `eslint` configurations (includes `eslint-config-prettier`) 14 | - `tsconfig`: `tsconfig.json`s used throughout the monorepo 15 | 16 | Each package is 100% [TypeScript](https://www.typescriptlang.org/). 17 | 18 | ### Services 19 | 20 | - `api1`: serverless service to say hello. 21 | - `api2`: serverless service to say ciao. 22 | 23 | Run `pnpm dev` to run in offline mode (from the service or the root level). 24 | 25 | ### Utilities 26 | 27 | This turborepo has some additional tools already setup for you: 28 | 29 | - [TypeScript](https://www.typescriptlang.org/) for static type checking 30 | - [ESLint](https://eslint.org/) for code linting 31 | - [Prettier](https://prettier.io) for code formatting 32 | - [Github Actions](https://github.com/features/actions) for CI. 33 | - [vitest](https://vitest.dev/) for testing. 34 | 35 | ### Build 36 | 37 | To build all apps and packages, run the following command: 38 | 39 | ```bash 40 | cd my-turborepo 41 | pnpm run build 42 | ``` 43 | 44 | ### Develop 45 | 46 | To develop all apps and packages, run the following command: 47 | 48 | ```bash 49 | cd my-turborepo 50 | pnpm run dev 51 | ``` 52 | 53 | ## Testing 54 | 55 | This template uses [vitest](https://vitest.dev/) as a testing framework. 56 | 57 | ```bash 58 | cd my-turborepo 59 | pnpm test # to run the tests, with coverage 60 | pnpm test:watch # to run tests in watch mode 61 | ``` 62 | 63 | ## Deployment 64 | 65 | Make sure to have `AWS_PROFILE` set up to the aws profile you want to deploy with. Otherwise the command will fail. 66 | Run `aws configure list-profiles` to see your profiles available. 67 | 68 | This template simulates two environments (staging and production): 69 | 70 | ```bash 71 | cd my-turborepo 72 | AWS_PROFILE= pnpm deploy:staging 73 | AWS_PROFILE= pnpm deploy:production 74 | ``` 75 | 76 | ⚠️ Running this command will deploy to AWS. Please make sure this is what you intend and beware of unintentional usage. 77 | 78 | ## Contributing 79 | 80 | Feel free to open a PR, file an issue. I'll happily look into it. 81 | 82 | ### Remote Caching 83 | 84 | See [Remote Caching (Beta)](https://turborepo.org/docs/core-concepts/remote-caching) for more info. 85 | 86 | ## Get started 87 | 88 | - clone this repo, or use it as template. 89 | - install [pnpm](https://pnpm.io/installation) for package management. 90 | - install the dependencies: `pnpm i`. 91 | - build and run the services: `pnpm build && pnpm dev`. 92 | - send first request: 93 | 94 | ```bash 95 | curl -X POST http://localhost:3000/dev/hello -H "Content-Type: application/json" -d '{"name": "World"}' 96 | ``` 97 | 98 | ```json 99 | { "message": "Hello World !" } 100 | ``` 101 | 102 | ```bash 103 | curl -X POST http://localhost:4000/dev/ciao -H "Content-Type: application/json" -d '{"name": "world"}' 104 | ``` 105 | 106 | ```json 107 | { "message": "Ciao World !" } 108 | ``` 109 | 110 | ## TODO 111 | 112 | These are a list of features in the pipeline: 113 | 114 | - [ ] documentation and guidelines on how to add a new api or service. 115 | - [ ] automatic changelog generation and release of packages with `changesets`. 116 | - [ ] auto-linting and formatting on save for faster development. 117 | - [ ] automatic deployment when merging to main. 118 | - [ ] website for documentation. 119 | 120 | ## Inspiration 121 | 122 | [Initial issue](https://github.com/vercel/turbo/issues/221). 123 | --------------------------------------------------------------------------------