Photo by vackground.com on Unsplash

Using Plop to improve our Serverless Developer Experience

This article covers using the Plop framework to create a CLI which can auto generate our scaffolded serverless code; with code examples written in TypeScript and the AWS CDK.

Serverless Advocate
8 min readMay 18, 2023

--

Preface

✔️ We create a CLI to autogenerate our serverless clean code app. 🔩
✔️ We demo this using a video and also walk through the code. 🧑🏽‍💻

Introduction 👋🏽

There are two key areas that I am personally passionate about when it comes to Serverless:

Developer Experience + Clean Code

In this article we will cover using Plop to create a basic Node CLI which will autogenerate our AWS CDK app and code based on the lightweight clean code approach which I previously discussed, which will reduce cognitive load on teams, speed up development, and ensure that we have agreed structure and consistency across our teams (the previous article on clean code below)

Note: The article above has a matching github repo which
can be found here for the lightweight approach:

https://github.com/leegilmorecode/serverless-clean-code-experience

Let’s see this basic CLI in action! 👨‍🚀

Enough talking, let’s see it in action before discussing how we would run this:

Hat-tip Allen Helton on the use of Amazon Polly!

👇 Before we go any further — please connect with me on LinkedIn for future blog posts and Serverless news https://www.linkedin.com/in/lee-james-gilmore/

Let’s run the CLI 👨‍🚀

Note: This is a very basic example and is not production
ready at all! It is simply to show a serverless CLI in action
using Plop to autogenerate our code in the correct structure
using basic templating! Don't @ me!

Let’s start by creating a basic folder called ‘customer-service’ alongside our ‘advocate-cli’ folder as shown below, which is going to house our new service:

We create a new folder called ‘customer-service’ for our autogenerated service

In the ‘advocate-cli’ folder we run npm i, npm run build, and npm run link.

Note: This will install the dependencies of the CLI, 
build it, and the link command will install the
CLI module as if we had pushed it to npm. In reality,
if we were productionising this app we would publish it to NPM,
but for now we will use npm link as a facade..

If we now go into the ‘customer-service’ folder we can run ‘advocate’ which will show the menu as shown below:

The menu options of our CLI

We start by choosing ‘Create a new CDK app’, and you will see that it will create a standard AWS CDK app in our ‘customer-service’ folder.

Creating a new CDK app using the Advocate CLI

Now we do the same again but choose ‘Create all base deps’ which will:

Our menu option for creating all the base dependencies, config and shared libraries

✔️ Install all of the base NPM dependencies (and dev dependencies) which are:

uuid @aws-lambda-powertools/metrics @aws-lambda-powertools/tracer @aws-lambda-powertools/logger @middy/core ajv ajv-formats @types/aws-lambda @types/uuid esbuild

✔️ Deletes the ‘lib’ folder and creates two new stacks for stateful and stateless stacks.

Our new stateful and stateless stacks

✔️ Repoints the AWS CDK app to the new stacks i.e. our bin/customer-service.ts file.

Our AWS CDK app now automatically points to our new stacks

✔️ Creates custom errors to use in our Lambda adapters and Use Cases:

Our new autogenerated errors, with an example of resource-not-found below
export class ResourceNotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = 'ResourceNotFound';
}
}

✔️ Creates functions to export the Lambda Powertools Logger as a singleton, a generic schema validation function, and an error-handler function which ensures we return the correct responses based on the error type.

Our autogenerated utils

Adding our first adapter ➕

OK, so we have a new AWS CDK app with all of our base dependencies generated by the advocate CLI; but now lets add a new Primary Adapter for creating a new customer using our CLI:

Adding our first Primary Adapter for Creating a Customer

We can see that it has created for us:

✔️ A new primary adapter called ‘create-customer.adapter.ts’ which has the following code:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import {
MetricUnits,
Metrics,
logMetrics,
} from '@aws-lambda-powertools/metrics';
import { Tracer, captureLambdaHandler } from '@aws-lambda-powertools/tracer';
import { logger, schemaValidator, errorHandler } from '@shared/index';

import { CreateCustomerDto } from '@dto/create-customer';
import { ValidationError } from '@errors/validation-error';
import { createCustomerUseCase } from '@use-cases/create-customer';
import { injectLambdaContext } from '@aws-lambda-powertools/logger';
import middy from '@middy/core';
import { schema } from './create-customer.schema';

const tracer = new Tracer();
const metrics = new Metrics();

// (primary adapter) --> use case --> (secondary adapter)
export const createCustomerAdapter = async ({
body,
}: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
if (!body) throw new ValidationError('no payload body');

const customer = JSON.parse(body) as CreateCustomerDto;

schemaValidator(schema, customer);

const created: CreateCustomerDto = await createCustomerUseCase(customer);

metrics.addMetric('SuccessfulCreateCustomerCreated', MetricUnits.Count, 1);

return {
statusCode: 201,
body: JSON.stringify(created),
};
} catch (error) {
let errorMessage = 'Unknown error';
if (error instanceof Error) errorMessage = error.message;
logger.error(errorMessage);

metrics.addMetric('CreateCustomerCreatedError', MetricUnits.Count, 1);

return errorHandler(error);
}
};

export const handler = middy(createCustomerAdapter)
.use(injectLambdaContext(logger))
.use(captureLambdaHandler(tracer))
.use(logMetrics(metrics));

✔️ It has created the Use Case that the adapter will call, which is called ‘create-customer.ts’ as shown below:

// import * as event from '@events/createCustomer';
import { CreateCustomerDto } from '@dto/create-customer';
import { getISOString, logger, schemaValidator } from '@shared/index';
import { schema } from '@schemas/customer';
import { v4 as uuid } from 'uuid';

// primary adapter --> (use case) --> secondary adapter(s)

/**
* Add description here
* Input: ?
* Output: ?
*
* Primary course:
*
* 1. step one
* 2. step two
*/
export async function createCustomerUseCase(
customer: CreateCustomerDto
): Promise<CreateCustomerDto> {
const createdDate = getISOString();

const customerDto: CreateCustomerDto = {
id: uuid(),
created: createdDate,
...customer,
}

schemaValidator(schema, customer);

// TODO - import and call secondary adapters

// TODO publish the correct event
// await publishEvent(
// customer,
// event.eventName,
// event.eventSource,
// event.eventVersion,
// createdDate
// );
logger.info(
`customer created event published`
);

return customerDto;
}

✔️ It has created the relevant DTO objects and schema files for validation, for example the ‘create-customer.schema.ts’ file with the following contents ready for the engineers to add into:

export const schema = {
type: 'object',
required: [],
maxProperties: 10, // TODO - amend base schema
minProperties: 0,
properties: {}
};

This now means that the engineering teams in less than 45 seconds have a fully fledged serverless application confining to the lightweight clean code structure, ready to add the business logic!

So how can we use this autogenerated code? 🚀

The code below shows how we might quickly augment the auto-generated stateless stack, by creating an API Gateway and attaching our autogenerated primary adapter:

import * as apigw from 'aws-cdk-lib/aws-apigateway';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs';
import * as path from 'path';

import { Construct } from 'constructs';
import { Tracing } from 'aws-cdk-lib/aws-lambda';

export class OrderServiceStatelessStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

const lambdaPowerToolsConfig = {
LOG_LEVEL: 'DEBUG',
POWERTOOLS_LOGGER_LOG_EVENT: 'true',
POWERTOOLS_LOGGER_SAMPLE_RATE: '1',
POWERTOOLS_TRACE_ENABLED: 'enabled',
POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS: 'captureHTTPsRequests',
POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'captureResult',
// This is your service👇
POWERTOOLS_SERVICE_NAME: 'CustomerService',
POWERTOOLS_METRICS_NAMESPACE: 'CustomerService',
};

const createLambda: nodeLambda.NodejsFunction =
new nodeLambda.NodejsFunction(this, 'CreateLambda', {
runtime: lambda.Runtime.NODEJS_16_X,
entry: path.join(
__dirname,
'src/adapters/primary/create-customer/create-customer.adapter.ts'
),
memorySize: 1024,
handler: 'handler',
tracing: Tracing.ACTIVE,
bundling: {
minify: true,
externalModules: ['aws-sdk'],
},
environment: {
...lambdaPowerToolsConfig,
},
});

const api: apigw.RestApi = new apigw.RestApi(this, 'Api', {
deploy: true,
deployOptions: {
stageName: 'prod',
loggingLevel: apigw.MethodLoggingLevel.INFO,
},
});

// This is your resource type 👇
const customers: apigw.Resource = api.root.addResource('customers');

customers.addMethod(
'POST',
new apigw.LambdaIntegration(createLambda, {
proxy: true,
})
);
}
}

We can then simply run cdk deploy in the customer-service folder to deploy the service (and all of our autogenerated code!).

If we now use Postman you will see that we can create a new customer as shown below by hitting our deployed service:

An example of utilising our autogenerated function

If there is appetite to use a fully fledged production version of this CLI please let me know in the comments and I will pick it up!

Wrapping up 👋

Please go and subscribe on my YouTube channel for similar content!

I would love to connect with you also on any of the following:

https://www.linkedin.com/in/lee-james-gilmore/
https://twitter.com/LeeJamesGilmore

If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋

Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)

About me

Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect and Global Serverless Architect based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.

I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture and technology.

*** The information provided are my own personal views and I accept no responsibility on the use of the information. ***

You may also be interested in the following:

--

--

Global Head of Technology & Architecture | Serverless Advocate | Mentor | Blogger | AWS x 7 Certified 🚀