├── .github └── workflows │ ├── dev-stage.yaml │ └── prod-stage.yaml ├── .gitignore ├── README.md ├── api └── index.js ├── drizzle.config.js ├── package-lock.json ├── package.json ├── public ├── .gitkeep └── index.html ├── reference └── serverless-iam-policy.md ├── serverless-nodejs-api.code-workspace ├── serverless.yml ├── src ├── cli │ ├── migrator.js │ └── putSecrets.js ├── db │ ├── clients.js │ ├── crud.js │ ├── schemas.js │ └── validators.js ├── index.js ├── lib │ └── secrets.js └── migrations │ ├── 0000_common_wolfpack.sql │ ├── 0001_quick_misty_knight.sql │ └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ └── _journal.json └── vercel.json /.github/workflows/dev-stage.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Dev Stage 2 | 3 | on: 4 | push: 5 | branches: [ dev ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | env: 12 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 13 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 14 | API_KEY: ${{ secrets.NEON_API_KEY }} 15 | STAGE: dev 16 | GH_TOKEN: ${{ github.token }} # github cli -> gh 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Use Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: "20.11" 25 | cache: 'npm' 26 | - name: Install dependencies 27 | run: npm install 28 | - name: Install neonctl and tsx 29 | run: npm install -g neonctl tsx 30 | - name: Delete Previous Branch 31 | run: neonctl branches delete dev --api-key ${{ env.API_KEY }} 32 | continue-on-error: true 33 | - name: Create Branched Database 34 | run: neonctl branches create --name dev --api-key ${{ env.API_KEY }} 35 | - name: Branch Connection String in Parameter Store 36 | run: | 37 | export DB_URL=$(neonctl connection-string --branch dev --api-key ${{ env.API_KEY }}) 38 | npx tsx src/cli/putSecrets.js dev $DB_URL 39 | - name: Deploy dev stage 40 | run: | 41 | npm run deploy-dev-stage 42 | - name: Dev Stage Pull Request 43 | run: | 44 | export PR_BRANCH=$(git branch --show-current) 45 | export DEFAULT_BRANCH=$(git remote show origin | awk '/HEAD branch/ {print $NF}') 46 | echo "$PR_BRANCH and $DEFAULT_BRANCH" 47 | export DEV_STAGE_INFO=$(npm run info-dev-stage) 48 | gh pr create --title "Automated PR from Dev Stage" --body "$DEV_STAGE_INFO" --base $DEFAULT_BRANCH --head $PR_BRANCH --repo $GITHUB_REPOSITORY -------------------------------------------------------------------------------- /.github/workflows/prod-stage.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy Production App 2 | 3 | on: 4 | # push: 5 | # branches: [ main ] 6 | pull_request: 7 | types: [ closed ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | if: github.event.pull_request.merged == true 13 | runs-on: ubuntu-latest 14 | env: 15 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 16 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 17 | STAGE: prod 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: "20.11" 24 | cache: 'npm' 25 | - name: Install dependencies 26 | run: npm install 27 | - name: Deploy 28 | run: npm run deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .serverless 3 | .env* 4 | .DS_Store 5 | reference/policy.json 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Node.js API with AWS Lambda & Neon Postgres 2 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import { app } from '../src/index' 2 | 3 | export default app -------------------------------------------------------------------------------- /drizzle.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | schema: './src/db/schemas.js', 3 | out: './src/migrations' 4 | } 5 | 6 | export default config -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-nodejs-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "dev": "serverless offline --stage dev", 7 | "info": "serverless info --stage prod --region us-east-2", 8 | "deploy": "serverless deploy --stage prod --region us-east-2", 9 | "deploy-dev-stage": "serverless deploy --stage dev --region us-east-2", 10 | "info-dev-stage": "serverless info --stage prod --region us-east-2", 11 | "remove": "serverless remove --stage prod --region us-east-2", 12 | "generate": "drizzle-kit generate:pg --config=drizzle.config.js", 13 | "migrate": "tsx src/cli/migrator.js", 14 | "vercel-build": "echo 'hello'" 15 | }, 16 | "dependencies": { 17 | "@aws-sdk/client-ssm": "^3.499.0", 18 | "@neondatabase/serverless": "^0.7.2", 19 | "drizzle-orm": "^0.29.3", 20 | "express": "^4.18.2", 21 | "serverless-http": "^3.1.1", 22 | "zod": "^3.22.4" 23 | }, 24 | "devDependencies": { 25 | "dotenv": "^16.4.1", 26 | "drizzle-kit": "^0.20.13", 27 | "serverless-dotenv-plugin": "^6.0.0", 28 | "serverless-offline": "^13.3.3", 29 | "tsx": "^4.7.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/serverless-nodejs-api/75acf9d46f09fcba361249f557d4525102918c84/public/.gitkeep -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 |
Env workign?
-------------------------------------------------------------------------------- /reference/serverless-iam-policy.md: -------------------------------------------------------------------------------- 1 | # Serverless Framework IAM Policy 2 | 3 | Use the IAM policy (JSON data) for the [Serverless Framework](https://www.serverless.com/) with the AWS Provider for deploying Node.js apps as serverless functions on AWS Lambda. 4 | 5 | Replace `AWS_ID` with your AWS Account ID (e.g. `123456789`) which you can find under [AWS IAM](https://console.aws.amazon.com/iam/) in the console. 6 | 7 | Use [this gist](https://gist.github.com/codingforentrepreneurs/03f6ddb7ba284e4f82a6c66b3103feda) for the most up-to-date version. 8 | 9 | 10 | `serverless-framework-iam-policy.json` 11 | ```json 12 | { 13 | "Version": "2012-10-17", 14 | "Statement": [ 15 | { 16 | "Effect": "Allow", 17 | "Action": [ 18 | "cloudformation:List*", 19 | "cloudformation:Get*", 20 | "cloudformation:ValidateTemplate", 21 | "ssm:*" 22 | ], 23 | "Resource": [ 24 | "*" 25 | ] 26 | }, 27 | { 28 | "Effect": "Allow", 29 | "Action": [ 30 | "cloudformation:CreateStack", 31 | "cloudformation:CreateUploadBucket", 32 | "cloudformation:DeleteStack", 33 | "cloudformation:Describe*", 34 | "cloudformation:UpdateStack", 35 | "cloudformation:CreateChangeSet", 36 | "cloudformation:ListChangeSets", 37 | "cloudformation:DeleteChangeSet", 38 | "cloudformation:ExecuteChangeSet" 39 | ], 40 | "Resource": [ 41 | "arn:aws:cloudformation:*:AWS_ID:stack/serverless-*" 42 | ] 43 | }, 44 | { 45 | "Effect": "Allow", 46 | "Action": [ 47 | "lambda:Get*", 48 | "lambda:List*", 49 | "lambda:CreateFunction", 50 | "lambda:TagResource", 51 | "lambda:UntagResource" 52 | ], 53 | "Resource": [ 54 | "*" 55 | ] 56 | }, 57 | { 58 | "Effect": "Allow", 59 | "Action": [ 60 | "s3:GetBucketLocation", 61 | "s3:CreateBucket", 62 | "s3:DeleteBucket", 63 | "s3:ListBucket", 64 | "s3:GetBucketPolicy", 65 | "s3:PutBucketPolicy", 66 | "s3:ListBucketVersions", 67 | "s3:PutAccelerateConfiguration", 68 | "s3:GetEncryptionConfiguration", 69 | "s3:PutEncryptionConfiguration", 70 | "s3:DeleteBucketPolicy", 71 | "s3:PutBucketTagging", 72 | "s3:UntagResource", 73 | "s3:TagResource", 74 | "s3:GetBucketTagging", 75 | "s3:ListTagsForResource" 76 | ], 77 | "Resource": [ 78 | "arn:aws:s3:::serverless-*serverlessdeploy*" 79 | ] 80 | }, 81 | { 82 | "Effect": "Allow", 83 | "Action": [ 84 | "s3:PutObject", 85 | "s3:GetObject", 86 | "s3:DeleteObject" 87 | ], 88 | "Resource": [ 89 | "arn:aws:s3:::serverless-*serverlessdeploy*" 90 | ] 91 | }, 92 | { 93 | "Effect": "Allow", 94 | "Action": [ 95 | "lambda:AddPermission", 96 | "lambda:CreateAlias", 97 | "lambda:DeleteFunction", 98 | "lambda:InvokeFunction", 99 | "lambda:PublishVersion", 100 | "lambda:RemovePermission", 101 | "lambda:Update*" 102 | ], 103 | "Resource": [ 104 | "arn:aws:lambda:*:AWS_ID:function:serverless-*" 105 | ] 106 | }, 107 | { 108 | "Effect": "Allow", 109 | "Action": [ 110 | "cloudwatch:GetMetricStatistics" 111 | ], 112 | "Resource": [ 113 | "*" 114 | ] 115 | }, 116 | { 117 | "Action": [ 118 | "logs:CreateLogGroup", 119 | "logs:CreateLogStream", 120 | "logs:DeleteLogGroup", 121 | "logs:TagResource", 122 | "logs:UntagResource" 123 | ], 124 | "Resource": [ 125 | "arn:aws:logs:*:AWS_ID:*" 126 | ], 127 | "Effect": "Allow" 128 | }, 129 | { 130 | "Action": [ 131 | "logs:PutLogEvents" 132 | ], 133 | "Resource": [ 134 | "arn:aws:logs:*:AWS_ID:*" 135 | ], 136 | "Effect": "Allow" 137 | }, 138 | { 139 | "Effect": "Allow", 140 | "Action": [ 141 | "logs:DescribeLogStreams", 142 | "logs:DescribeLogGroups", 143 | "logs:FilterLogEvents" 144 | ], 145 | "Resource": [ 146 | "*" 147 | ] 148 | }, 149 | { 150 | "Effect": "Allow", 151 | "Action": [ 152 | "events:Put*", 153 | "events:Remove*", 154 | "events:Delete*" 155 | ], 156 | "Resource": [ 157 | "arn:aws:events:*:AWS_ID:rule/serverless-*" 158 | ] 159 | }, 160 | { 161 | "Effect": "Allow", 162 | "Action": [ 163 | "events:DescribeRule" 164 | ], 165 | "Resource": [ 166 | "arn:aws:events:*:AWS_ID:rule/serverless-*" 167 | ] 168 | }, 169 | { 170 | "Effect": "Allow", 171 | "Action": [ 172 | "iam:PassRole" 173 | ], 174 | "Resource": [ 175 | "arn:aws:iam::AWS_ID:role/serverless-*" 176 | ] 177 | }, 178 | { 179 | "Effect": "Allow", 180 | "Action": [ 181 | "iam:GetRole", 182 | "iam:CreateRole", 183 | "iam:TagRole", 184 | "iam:PutRolePolicy", 185 | "iam:DeleteRolePolicy", 186 | "iam:DeleteRole" 187 | ], 188 | "Resource": [ 189 | "arn:aws:iam::AWS_ID:role/serverless-*" 190 | ] 191 | }, 192 | { 193 | "Effect": "Allow", 194 | "Action": [ 195 | "apigateway:*" 196 | ], 197 | "Resource": [ 198 | "arn:aws:apigateway:*::/apis*", 199 | "arn:aws:apigateway:*::/restapis*", 200 | "arn:aws:apigateway:*::/apikeys*", 201 | "arn:aws:apigateway:*::/tags*", 202 | "arn:aws:apigateway:*::/usageplans*" 203 | ] 204 | }, 205 | { 206 | "Effect": "Allow", 207 | "Action": [ 208 | "tag:*" 209 | ], 210 | "Resource": [ 211 | "*" 212 | ] 213 | } 214 | ] 215 | } 216 | ``` 217 | -------------------------------------------------------------------------------- /serverless-nodejs-api.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-nodejs-api 2 | frameworkVersion: '3' 3 | useDotenv: true 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs20.x 8 | environment: 9 | DEBUG: ${env:DEBUG, 0} 10 | STAGE: ${env:STAGE, "prod"} 11 | iam: 12 | role: 13 | name: serverless-my-ssm-role-${env:STAGE, "prod"} 14 | statements: 15 | - Effect: 'Allow' 16 | Resource: '*' 17 | Action: 18 | - "ssm:GetParameter" 19 | - "ssm:GetParameters" 20 | - "ssm:GetParametersByPath" 21 | - "ssm:GetParameterHistory" 22 | - "ssm:DescribeParameters" 23 | 24 | functions: 25 | api: 26 | handler: src/index.handler 27 | events: 28 | - httpApi: '*' 29 | 30 | custom: 31 | dotenv: 32 | exclude: 33 | - AWS_ACCESS_KEY_ID 34 | - AWS_SECRET_ACCESS_KEY 35 | - AWS_SESSION_TOKEN 36 | - DATABASE_URL 37 | 38 | plugins: 39 | - serverless-offline 40 | - serverless-dotenv-plugin 41 | -------------------------------------------------------------------------------- /src/cli/migrator.js: -------------------------------------------------------------------------------- 1 | // tsx src/cli/migrator.js 2 | const { drizzle } = require('drizzle-orm/neon-serverless') 3 | const {migrate} = require('drizzle-orm/postgres-js/migrator') 4 | const schema = require('../db/schemas') 5 | const secrets = require('../lib/secrets') 6 | require('dotenv').config() 7 | 8 | const { Pool, neonConfig } = require('@neondatabase/serverless'); 9 | 10 | const ws = require('ws'); 11 | 12 | async function performMigration() { 13 | const dbUrl = await secrets.getDatabaseUrl() 14 | if (!dbUrl) { 15 | return 16 | } 17 | // neon serverless pool 18 | // https://github.com/neondatabase/serverless?tab=readme-ov-file#pool-and-client 19 | neonConfig.webSocketConstructor = ws; // <-- this is the key bit 20 | const pool = new Pool({ connectionString: dbUrl }); 21 | pool.on('error', err => console.error(err)); // deal with e.g. re-connect 22 | // ... 23 | const client = await pool.connect(); 24 | try { 25 | await client.query('BEGIN'); 26 | const db = await drizzle(client, {schema}) 27 | await migrate(db, {migrationsFolder: 'src/migrations'}) 28 | await client.query('COMMIT'); 29 | } catch (err) { 30 | await client.query('ROLLBACK'); 31 | throw err; 32 | 33 | } finally { 34 | client.release(); 35 | } 36 | await pool.end() 37 | 38 | } 39 | 40 | 41 | if (require.main === module) { 42 | console.log("run Migrations!") 43 | performMigration().then((val)=>{ 44 | console.log("Migrations done") 45 | process.exit(0) 46 | }).catch(err=>{ 47 | console.log('Migrations error') 48 | process.exit(1) 49 | }) 50 | } -------------------------------------------------------------------------------- /src/cli/putSecrets.js: -------------------------------------------------------------------------------- 1 | // github actions cli command 2 | // tsx src/cli/putSecrets.js