DEV Community

Cover image for Serverless Framework Deployment: Unleash the Power of AWS Lambda
Sahil Khurana
Sahil Khurana

Posted on • Originally published at innostax.com

Serverless Framework Deployment: Unleash the Power of AWS Lambda

Let me tell you exactly what happened the first time I tried to set up Lambda manually.

Four hours. IAM trust policies I didn't fully understand, ARNs copy-pasted into the wrong fields, an API Gateway that was technically configured but somehow not routing anything correctly, and a deploy that failed with an error message pointing me nowhere useful. I hadn't written a single line of actual business logic yet.

That's when someone on my team mentioned the Serverless Framework. My first reaction was honestly skepticism — another abstraction layer sounded like another thing to learn and eventually fight with. I was wrong about that.

This isn't a "look how clean this tool is" post. It's more like: here's what I actually did to get a Postgres-backed CRUD API running on Lambda, step by step, including the parts that tripped me up.

What the Framework Is Actually Doing Under the Hood

Worth knowing before you start: the Serverless Framework isn't magic. It's generating CloudFormation templates and submitting them to AWS on your behalf. Your Lambda functions, API Gateway routes, CloudWatch log groups — all of it gets provisioned from a single config file.

It works with other providers too, but the AWS integration is where it really earns its keep. The console clicking and manual ARN-wiring that burns time at the start of every serverless project? Gone. Same deploy workflow whether you're building a REST API, an event processor, or a cron job. Once you've done it once, the second project takes a fraction of the time.

What You're Building

Four live endpoints backed by PostgreSQL. A Users table. Create, read, update, delete — nothing exotic, but a real enough foundation that you can extend it into something actual once this guide is done.

You'll need an AWS account, the AWS CLI installed, and the Serverless Framework installed before starting. That's it.

Step 1: Sort Out Your AWS Credentials

Run this to create both config files in one go:

bash
cat <<EOF > ~/.aws/credentials
[default]
aws_access_key_id = <REPLACE_WITH_YOUR_SECRET_KEY>
aws_secret_access_key = <REPLACE_WITH_YOUR_ACCESS_KEY>
EOF

cat <<EOF > ~/.aws/config
[default]
region = eu-west-1
output = json
EOF
Enter fullscreen mode Exit fullscreen mode

Replace the placeholders with your actual keys. I'm using eu-west-1 — swap in whatever region makes sense for where you're deploying.

Step 2: The IAM Role (Seriously, Don't Skip This)

Okay so this is the one that burned me the first time around. The Serverless Framework needs an IAM role with the right permissions to provision resources during deployment. If you skip this, the deploy fails. The error you'll get doesn't say "hey you're missing an IAM role" — it says something vague about permissions and you'll spend a while chasing the wrong thing.
Create the role:

bash
aws iam create-role --role-name serverlessLabs --assume-role-policy-document '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "lambda.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}'
Enter fullscreen mode Exit fullscreen mode

Attach the Lambda execution policy to it:

bash
aws iam attach-role-policy --role-name serverlessLabs \
  --policy-arn arn:aws:iam::aws:policy/AWSLambda_FullAccess
Enter fullscreen mode Exit fullscreen mode

Verify it's actually there:

bash
aws iam get-role --role-name serverlessLabs
Enter fullscreen mode Exit fullscreen mode

If you see role details, good. If you get an error, fix it here rather than finding out it's broken in step 7.

Step 3: Project Setup

bash
mkdir node-crud
cd node-crud
npm install -g serverless
npm i prisma
Then init Prisma:
bash
npx prisma init
Enter fullscreen mode Exit fullscreen mode

Two things get created — a prisma/ directory with schema.prisma inside it, and a .env file at the root. The .env is where your database URL goes. Add it to .gitignore right now. Not later. Now.

Step 4: Database Schema
Open prisma/schema.prisma:

prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Users {
  id    String  @id @default(uuid())
  name  String? @db.VarChar(255)
  email String? @db.VarChar(255)
}
Enter fullscreen mode Exit fullscreen mode

Drop your connection string in .env:

env
DATABASE_URL="postgresql://<username>:<password>@<hosted dbURL>:5432/mydb?schema=public"
Now run these three, in this order, all three:
bash
npx prisma format
npx prisma generate
npx prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

format tidies the schema file. generate builds the Prisma client your functions will import. migrate dev actually creates the Users table in your database. If migrate dev fails, stop here and figure out why — your connection string is probably wrong.

Step 5: The Handler Functions

handler.js at the project root:

javascript
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

module.exports.create = async (event) => {
  try {
    const { name, email } = JSON.parse(event.body);
    const newUser = await prisma.Users.create({ data: { name, email } });
    return { statusCode: 200, body: JSON.stringify(newUser) };
  } catch (error) {
    return { statusCode: 500, body: JSON.stringify({ error: "Failed to create user: " + error.message }) };
  }
};

module.exports.read = async () => {
  try {
    const users = await prisma.Users.findMany();
    return { statusCode: 200, body: JSON.stringify(users) };
  } catch (error) {
    return { statusCode: 500, body: JSON.stringify({ error: "Failed to fetch users: " + error.message }) };
  }
};

module.exports.update = async (event) => {
  try {
    const { id } = event.pathParameters;
    const { name, email } = JSON.parse(event.body);
    const updatedUser = await prisma.Users.update({
      where: { id: parseInt(id) },
      data: { name, email },
    });
    return { statusCode: 200, body: JSON.stringify(updatedUser) };
  } catch (error) {
    return { statusCode: 500, body: JSON.stringify({ error: "Failed to update user: " + error.message }) };
  }
};

module.exports.delete = async (event) => {
  try {
    const { id } = event.pathParameters;
    const deletedUser = await prisma.Users.delete({ where: { id: parseInt(id) } });
    return { statusCode: 200, body: JSON.stringify({ message: "User deleted successfully", deletedUser }) };
  } catch (error) {
    return { statusCode: 500, body: JSON.stringify({ error: "Failed to delete user: " + error.message }) };
  }
};
Enter fullscreen mode Exit fullscreen mode

Same structure across all four — try the operation, return 200 with the result, catch and return 500 with the actual error message. I've debugged enough Lambda functions that swallow errors silently to know: always include error.message in the 500 response. Future you will appreciate it.

Step 6: serverless.yml

Wipe whatever's in there and replace with this:

yaml
service: postgres-crud-api
provider:
  name: aws
  runtime: nodejs18.x
functions:
  create:
    handler: handler.create
    events:
      - http:
          path: create
          method: post
  read:
    handler: handler.read
    events:
      - http:
          path: read
          method: get
  update:
    handler: handler.update
    events:
      - http:
          path: update/{id}
          method: put
  delete:
    handler: handler.delete
    events:
      - http:
          path: delete/{id}
          method: delete
Enter fullscreen mode Exit fullscreen mode

The provider block sets the target cloud and runtime — Node 18.x here, though honestly check if there's a newer LTS available by the time you're reading this.
functions is where each handler export gets mapped to a route and HTTP method. API Gateway configuration is derived automatically from these entries — you're not touching it anywhere else.

The {id} segments in the update and delete paths become event.pathParameters.id inside the handler. So a request to /update/some-uuid hands you "some-uuid" to work with. That's all there is to it.

Step 7: Deploy

bash
sls deploy
Enter fullscreen mode Exit fullscreen mode

Grab a coffee. Two minutes, give or take. When it finishes, your terminal shows the live API Gateway URLs for all four endpoints. Pop open the AWS console if you want to see what was built — Lambda functions, API Gateway config, CloudFormation stack, all of it created without you touching the console once.

Step 8: Testing

Postman, cURL, doesn't matter. Here's the order I'd go in:

GET /read first. Should return an empty array. If it throws an error instead, something's broken in the Lambda-to-Prisma connection and you want to know before you try writing data.

POST /create — send { "name": "Jane", "email": "jane@example.com" }. Get back a user object with an id? Prisma is talking to Postgres. Lambda is executing. That's the hard part confirmed working.

PUT /update/{id} — use the id from whatever create returned. Change the name. Verify the response reflects it.

DELETE /delete/{id} — the response includes the record that got deleted, so you can confirm you removed the right one.

Create works, read returns it — the full chain is functioning. Everything else from here is adding more of the same.

What's Next

The setup is the slow part. It's done. Adding a new endpoint now is: write the handler function, add the route to serverless.yml, run sls deploy. That's genuinely it.

Natural next steps: JWT auth if you're building anything that needs to be secured, environment variable separation for staging vs production (set it up early, not after you've already deployed to prod by accident), more Prisma models for additional tables, more complex query logic inside the existing handlers.

The deployment model doesn't change regardless of what you add to the application. That's the whole point of it.


At Innostax, we've helped engineering teams get through exactly this — serverless setup on AWS, Prisma + Postgres wiring, IAM configurations that don't silently break at deploy time — and built it into backends that hold up past the prototype stage. If you're figuring out the right serverless foundation for your project, innostax.com/contact is where that conversation happens.

Originally published on the Innostax Engineering Blog.

Top comments (0)