Goodbye CloudFormation YAML: Building Type-Safe AWS Backends with Amplify Gen 2
Building an Enterprise HRMS with AWS Serverless & GenAI — Part 1
Modern cloud applications require more than just application code. Behind every application are databases, authentication systems, APIs, queues, storage, permissions, and background processes.
Traditionally, developers could create these resources manually through the AWS Console or define them using CloudFormation templates.
But as an application grows, managing infrastructure this way can become difficult.
While building an Enterprise Human Resource Management System (HRMS), I wanted the infrastructure to be:
- Easy to maintain
- Version controlled
- Reproducible
- Type-safe
- Closely integrated with the application code
This is where AWS Amplify Gen 2 became an important part of our architecture.
What is Infrastructure as Code?
Infrastructure as Code (IaC) is the practice of defining cloud infrastructure using code instead of manually creating resources through a cloud provider’s console.
For example, instead of manually creating a DynamoDB table, we can describe the required infrastructure in code.
A traditional CloudFormation definition might look like:
Resources:
EmployeeTable:
Type: AWS::DynamoDB::TableThis works, but large applications can end up with large YAML or JSON templates that become harder to maintain.
For a TypeScript-based application, having infrastructure defined using the same programming language can provide a better development experience.
This is one of the ideas behind AWS CDK and Amplify Gen 2.
What is AWS Amplify Gen 2?
AWS Amplify Gen 2 provides a TypeScript-based approach to building and deploying cloud backends.
Instead of defining everything through traditional configuration files, developers can define AWS resources using TypeScript.
The important part is that Amplify Gen 2 doesn’t replace AWS infrastructure services.
It works with technologies such as:
- AWS CDK
- AWS CloudFormation
- AWS Lambda
- Amazon DynamoDB
- AWS AppSync
- Amazon Cognito
- Amazon S3
This gives us a developer-friendly layer for defining infrastructure while still using AWS’s underlying infrastructure services.
The basic idea
The general flow is:
TypeScript Infrastructure
↓
Amplify Gen 2
↓
AWS CDK
↓
CloudFormation
↓
AWS Resources
We write TypeScript, while AWS handles the underlying infrastructure provisioning.
Starting the HRMS Backend
In our HRMS project, the backend is organized inside the amplify directory.
A simplified structure looks like this:
amplify/
├── auth/
├── data/
├── functions/
└── backend.ts
Each directory has a specific responsibility.
For example:
auth/→ Authentication configurationdata/→ GraphQL API and data modelsfunctions/→ Lambda functionsbackend.ts→ Main backend infrastructure configuration
The backend.ts file becomes especially important when we need to combine Amplify resources with custom AWS infrastructure.
The definebackend() Function
One of the main concepts in Amplify Gen 2 is defineBackend().
Get Ashan kavinda’s stories in your inbox
Join Medium for free to get updates from this writer.
A simplified example looks like this:
import { defineBackend } from "@aws-amplify/backend";
export const backend = defineBackend({
auth,
data,
});Here, we are telling Amplify which backend resources belong to our application.
Instead of manually connecting each resource through the AWS Console, the backend is defined through code.
This also means the infrastructure can be committed to Git alongside the application.
That gives us some important advantages:
- Infrastructure changes can be reviewed.
- Previous versions can be tracked.
- Developers can work with the same backend configuration.
- Environments can be reproduced more consistently.
Where AWS CDK Comes In
One of the most useful features of Amplify Gen 2 is that we are not limited to predefined Amplify resources.
When our HRMS required additional AWS services, we could extend the backend using AWS CDK.
For example:
import * as sqs from "aws-cdk-lib/aws-sqs";
const notificationStack = backend.createStack("NotificationStack");
const notificationQueue = new sqs.Queue(
notificationStack,
"NotificationQueue",
{
visibilityTimeout: Duration.seconds(30),
retentionPeriod: Duration.days(4),
}
);This allows us to use native CDK constructs when we need more control over AWS resources.
In our HRMS, this becomes particularly useful for services such as:
- Amazon SQS
- API Gateway
- IAM policies
- Other custom AWS resources
So instead of choosing between Amplify and CDK, we can use them together.
Amplify + CDK: The Best of Both Worlds
This became an important architectural idea for our HRMS.
Amplify provides convenient abstractions for common application resources, while CDK allows us to customize the infrastructure when necessary.
For example:
This hybrid approach allows the backend to grow without moving infrastructure into completely separate projects.
Why This Approach Worked for Our HRMS
The HRMS isn’t just a simple CRUD application.
As the project developed, we needed:
- Employee authentication
- Role-based access control
- GraphQL APIs
- DynamoDB data storage
- Lambda functions
- Email notifications
- Scheduled background jobs
- GitHub webhooks
- Telegram integration
- Google Calendar integration
- AI-powered HR features
Managing all these resources manually would make the infrastructure increasingly difficult to maintain.
Using Amplify Gen 2 gave us a single TypeScript-based backend project, while CDK allowed us to add AWS services that required more customization.
What I Learned
The biggest lesson for me was that Infrastructure as Code doesn’t have to mean writing large CloudFormation templates manually.
With Amplify Gen 2, I could work with AWS infrastructure using TypeScript, while still taking advantage of AWS CDK and CloudFormation underneath.
It also changed the way I thought about backend development.
Instead of thinking:
“First build the application, then configure AWS.”
I started thinking:
“The application and its infrastructure are part of the same system.”
That mindset becomes especially important when building serverless applications with many AWS services.
Conclusion
AWS Amplify Gen 2 provides a modern TypeScript-first approach to building cloud infrastructure.
For our HRMS, it allowed us to define the backend as code, keep infrastructure under version control, and extend the platform using native AWS CDK constructs whenever we needed more control.
The result is a backend that can grow from a simple application into a much more complex serverless architecture without abandoning the same infrastructure-as-code approach.
But defining infrastructure is only the beginning.
The next challenge was designing the data layer and deciding who should be allowed to access each piece of HR data.
Next: Part 2
Schema-Driven Security: GraphQL Data Modeling & RBAC with AWS AppSync and DynamoDB
In the next article, we’ll build the HRMS data layer and explore how Admin, Manager, and Employee roles can be enforced directly at the API level using AppSync authorization rules.









