How to use serverless framework in Node.js

Serverless Framework simplifies deploying and managing serverless functions across multiple cloud providers with declarative configuration and automated infrastructure management. As the creator of CoreUI, a widely used open-source UI library, I’ve deployed serverless Node.js applications for scalable backend services throughout my 12 years of development experience since 2014. The most efficient approach is defining functions and resources in serverless.yml configuration file and using Serverless CLI for deployment automation. This method provides infrastructure as code, multi-cloud support, and local development tools for testing functions before deployment.

Install Serverless Framework globally and create a new serverless service with AWS Lambda.

npm install -g serverless

serverless create --template aws-nodejs --path my-service
cd my-service
npm init -y
npm install

Configure serverless.yml for AWS Lambda functions:

# serverless.yml
service: my-serverless-api

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  stage: ${opt:stage, 'dev'}
  memorySize: 256
  timeout: 30
  environment:
    STAGE: ${self:provider.stage}
    TABLE_NAME: ${self:custom.tableName}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource:
            - Fn::GetAtt:
                - UsersTable
                - Arn

custom:
  tableName: users-table-${self:provider.stage}

functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: hello
          method: get
          cors: true

  createUser:
    handler: handlers/users.create
    events:
      - http:
          path: users
          method: post
          cors: true

  getUser:
    handler: handlers/users.get
    events:
      - http:
          path: users/{id}
          method: get
          cors: true

  listUsers:
    handler: handlers/users.list
    events:
      - http:
          path: users
          method: get
          cors: true

  updateUser:
    handler: handlers/users.update
    events:
      - http:
          path: users/{id}
          method: put
          cors: true

  deleteUser:
    handler: handlers/users.delete
    events:
      - http:
          path: users/{id}
          method: delete
          cors: true

  scheduledJob:
    handler: handlers/scheduled.cleanup
    events:
      - schedule:
          rate: cron(0 2 * * ? *)
          enabled: true

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST

plugins:
  - serverless-offline

Create handler functions:

// handler.js
export const hello = async (event) => {
  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    },
    body: JSON.stringify({
      message: 'Hello from Serverless!',
      timestamp: new Date().toISOString()
    })
  }
}
// handlers/users.js
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, PutCommand, GetCommand, ScanCommand, UpdateCommand, DeleteCommand } from '@aws-sdk/lib-dynamodb'

const client = new DynamoDBClient({})
const docClient = DynamoDBDocumentClient.from(client)
const tableName = process.env.TABLE_NAME

export const create = async (event) => {
  try {
    const data = JSON.parse(event.body)
    const id = Date.now().toString()

    const item = {
      id,
      name: data.name,
      email: data.email,
      createdAt: new Date().toISOString()
    }

    await docClient.send(new PutCommand({
      TableName: tableName,
      Item: item
    }))

    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(item)
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

export const get = async (event) => {
  try {
    const { id } = event.pathParameters

    const result = await docClient.send(new GetCommand({
      TableName: tableName,
      Key: { id }
    }))

    if (!result.Item) {
      return {
        statusCode: 404,
        body: JSON.stringify({ error: 'User not found' })
      }
    }

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(result.Item)
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

export const list = async (event) => {
  try {
    const result = await docClient.send(new ScanCommand({
      TableName: tableName
    }))

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify({
        users: result.Items,
        count: result.Count
      })
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

export const update = async (event) => {
  try {
    const { id } = event.pathParameters
    const data = JSON.parse(event.body)

    const result = await docClient.send(new UpdateCommand({
      TableName: tableName,
      Key: { id },
      UpdateExpression: 'set #name = :name, email = :email, updatedAt = :updatedAt',
      ExpressionAttributeNames: {
        '#name': 'name'
      },
      ExpressionAttributeValues: {
        ':name': data.name,
        ':email': data.email,
        ':updatedAt': new Date().toISOString()
      },
      ReturnValues: 'ALL_NEW'
    }))

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(result.Attributes)
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

export const deleteUser = async (event) => {
  try {
    const { id } = event.pathParameters

    await docClient.send(new DeleteCommand({
      TableName: tableName,
      Key: { id }
    }))

    return {
      statusCode: 204,
      headers: {
        'Access-Control-Allow-Origin': '*'
      }
    }
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message })
    }
  }
}

Install and use serverless-offline for local development:

npm install --save-dev serverless-offline
serverless offline start

Deploy to AWS:

# Deploy to dev stage
serverless deploy

# Deploy to production stage
serverless deploy --stage production

# Deploy single function
serverless deploy function --function createUser

# View logs
serverless logs --function createUser --tail

# Remove service
serverless remove

Here the serverless.yml defines service configuration with provider settings, function definitions, and AWS resources declaratively. The functions section maps handler functions to HTTP endpoints with automatic API Gateway configuration. The events array specifies triggers including HTTP requests, scheduled cron jobs, and AWS service events. The resources section defines CloudFormation resources like DynamoDB tables deployed with the service. The handler functions return objects with statusCode, headers, and body following AWS Lambda proxy integration format. The serverless-offline plugin enables local testing without deploying to AWS for faster development cycles. The deploy command packages functions, creates CloudFormation stack, and deploys all infrastructure automatically.

Best Practice Note:

This is the serverless deployment approach we use for CoreUI backend services requiring auto-scaling and pay-per-use pricing models. Use environment variables for stage-specific configuration and secrets management with AWS Systems Manager Parameter Store, implement proper error handling and logging using AWS CloudWatch for production monitoring, and consider implementing API Gateway request validation and throttling to protect functions from malicious traffic and unexpected cost spikes.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team