The future of web development: AWS Amplify’s Code First Approach
AWS Amplify‘s new code-first developer experience is contributing to shaping the future of web development. This approach emphasizes building with an app-first mentality, focusing on the seamless DX while leveraging the power of AWS services. By adopting this approach, developers can create robust and scalable full-stack applications that meet customer needs. This blog post delves into sources of friction developers often encounter when developing, extending, and shipping apps, and how Amplify solves these challenges. From code generation to enabling extensibility and streamlining deployment, Amplify is everything you need to build full-stack apps in the cloud.
In this article, we will explore the requirements and key benefits behind the AWS Amplify’s code-first approach. From generating code to enhancing extensibility and streamlining deployment.
Sources of Friction
When developers experience friction, it can lead to delays in delivering new features, resolving bugs, or addressing customer issues. This, in turn, affects the overall customer experience. The poor developer experience usually falls into one of 3 areas:
Develop
Your time as a web developer is spent building features for your web app. The tools and services from vendors that automate this stage or enhance the coding experience can greatly improve productivity and efficiency. However, if not intentionally designed, they can contribute to a poor experience.
Here are some popular considerations for great DX, that tools helping developers build software take into account:
- Eliminate redundancy: generate redundant code like CRUD APIs or UI components
- Seamless performance: get performance out of the box
- Type safety: TypeScript is the new default and for good reason.
Extend
Opinionated defaults, while helpful, enforce certain architectural decisions that you may outgrow in the future. In an attempt to enhance developer experience, vendors might lock you into defaults that don’t fit your style.
- Extensibility API: Not only should you be able to extend your API but vendors should expose a piece of the puzzle where your own piece fits right in
- Bring your own logic (BYOL): You should be able to reject a slice of a vendor’s API and bring your own logic instead.
Ship
If there’s one thing you should fully automate as a web developer, it should be product delivery. Unfortunately, many of us have skill gaps when it comes to setting up infrastructure, and that’s alright because your responsibilities are focused on building.
- Automation: A hosting feature should not be enough. You should be able to make a change and rollout happens automatically through a CI/CD pipeline
- Infra in your familiar language: Your experience setting up infrastructure should feel tailored to you as a web developer. You should configure a server with JS if you are a JavaScript developer and YAML if you understand and know how to write YAML.
Let’s discuss each DX considerations at a deeper level and also take a look at how AWS Amplify solves each.
Develop: Amplify’s Type Safe Everything
TypeScript’s popularity has skyrocketed recently, becoming the fifth most loved language in the 2023Stack Overflow Developer Survey. The popularity of TypeScript reflects reflects a broader trend in web development towards languages that offer both flexibility and robustness. Developers appreciate TypeScript’s ability to catch errors early, streamline collaboration, and provide a clear structure for large codebases.
Type safety helps catch errors during development rather than runtime, saving countless hours of debugging and testing. It also means more readable and maintainable code, which is essential in collaborative environments where clear communication through code is key.
In addition to TypeScript’s popularity and benefits in web development, isormophic capabilities in TypeScript are quickly emerging. Isomorphic TypeScript allows developers to author types that can both be accessed from the server code and from the client code. This means that developers can share code between the server and client, reducing duplication and improving code maintenance. AWS Amplify is now Type Safe First with all of these DX built in.
Develop: Automatic Performance
The ultimate reason we default to Nuxt.js or Next.js is not because of SSR or SSG, etc. It is because we want to give our customers a fantastic experience that is a result of 100/100 performance score. I advocate for you to care about performance beyond what Next.js, Astro, Nuxt or Remix does, but it is refreshing to not have to worry about getting the foundations right.
React and Vue produce JavaScript that can run in the browser, making them easy to deploy/serve amidst all the performance and possible SEO cost. But once you need to render a page on the server first before hydrating on a client, then you are going to need control over your SSR build process and output. This is where Amplify Hosting comes in. It allows you to use your SSR frameworks to build for performance without worrying about SSR deployment. You just need to connect your repository in most cases.
AWS Amplify offers out of the box support for Next.js apps and now you can host Nuxt apps too.
Develop: Code Gen
Implementing CRUD (create, read, update, delete) in web development is a repetitive task that often takes significant development time. To address this challenge, TypeScript developers have turned to code generators to automate these tasks and streamline their workflow.
Code generators are particularly beneficial for generating data models, API clients, UI components, and Form UIs. Automating these repetitive tasks lets you focus on more complex and creative aspects of your projects. Additionally, code generators can enhance developer experience (DX) by introducing type safety and improved code quality.
Amplify Gen 2 takes automation a step further by automatically generating logic and types as you write code, eliminating the need to run CLI commands explicitly. Gen 2 is TypeScript-first; it allows you to generate a “data client” for your frontend code to make fully-typed API requests to your backend. As a result, you can make CRUDL (Create, Read, Update, Delete, List) operations and escape the repetitive chores of writing CRUD functions.
The code snippet below shows how to define a schema with Gen 2. The defineData
function is used to set the schema and authorization modes for the data, illustrating how Gen 2 streamlines backend data model definitions with type safety.
// Backend import { type ClientSchema, a, defineData } from '@aws-amplify/backend'; const schema = a.schema({ Todo: a .model({ content: a.string(), done: a.boolean(), priority: a.enum(['low', 'medium', 'high']) }) .authorization([a.allow.owner()]), }); export type Schema = ClientSchema<typeof schema>; export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'userPool', apiKeyAuthorizationMode: { expiresInDays: 30, }, }, });
The following code snippet is a React component that utilizes the generateClient
function from aws-amplify/data
to create a typed client instance for interacting with the backend.
// Frontend
import { useState, useEffect } from 'react';
import { generateClient } from 'aws-amplify/data';
import { Schema } from '@/../amplify/data/resource';
export default function HomePage() {
const client = generateClient<Schema>();
const [todos, setTodos] = useState<Schema['Todo'][]>([]);
useEffect(() => {
async function listTodos() {
// fetch all todos
const { data } = await client.models.Todo.list();
setTodos(data);
}
listTodos();
}, []);
useEffect(() => {
const sub = client.models.Todo.observeQuery()
.subscribe(({ items }) => setTodos([...items]))
return () => sub.unsubscribe()
}, []);
return (
<main>
<h1>Hello, Amplify 👋</h1>
<button onClick={async () => {
// create a new Todo with the following attributes
const { errors, data: newTodo } = await client.models.Todo.create({
content: "This is the todo content",
done: false,
priority: 'medium'
})
console.log(errors, newTodo);
}}>Create </button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.content}</li>
))}
</ul>
</main>
);
}
Extend: Hooks
Hooks are specific implementation tools that enable extensibility. Extensibility means customization, scalability, future-proofing, efficiency, and collaboration.
Amplify Functions, backed by AWS Lambda, provide server-side extensibility. They allow you to extend your application’s backend capabilities without managing the underlying infrastructure. Amplify Gen 2 will enable developers to create custom functionalities for various events, such as user authentication in Amazon Cognito. Consider a situation where you want to run a business logic after a customer has signed up — Developers can create a post-confirmation trigger in Amazon Cognito, enhancing the authentication flow with custom serverless functions.
// amplify/backend.ts
import * as url from 'node:url';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource.js';
import { data } from './data/resource.js';
const backend = defineBackend({
auth,
data
});
// create function stack for the auth triggers
const authTriggerStack = backend.createStack('AuthTriggerStack');
// create the PostConfirmation trigger
const postConfirmationTrigger = new lambda.NodejsFunction(
authTriggerStack,
'PostConfirmation',
{
entry: url.fileURLToPath(
new URL('./custom/post-confirmation.ts', import.meta.url)
)
}
);
// add the newly created trigger to the auth resource
const userPool = backend.resources.auth.resources.userPool as cognito.UserPool;
userPool.addTrigger(
cognito.UserPoolOperation.POST_CONFIRMATION,
postConfirmationTrigger
);
Amplify hub events on the other hand provide frontend extensibility. It allows different parts of your frontend application to communicate with each other in real-time. This is particularly useful for creating dynamic and responsive user interfaces that must react promptly to application state changes or user interactions. For instance, when a user signs in, various components of the frontend can respond to this event immediately, updating the UI accordingly.
import { Hub } from 'aws-amplify/utils';
const hubListenerCancel = Hub.listen('auth', ({ payload }) => {
switch (payload.event) {
case 'signedIn':
console.log('user have been signedIn successfully.');
break;
case 'signedOut':
console.log('user have been signedOut successfully.');
break;
case 'tokenRefresh':
console.log('auth tokens have been refreshed.');
break;
case 'tokenRefresh_failure':
console.log('failure while refreshing auth tokens.');
break;
case 'signInWithRedirect':
console.log('signInWithRedirect API has successfully been resolved.');
break;
case 'signInWithRedirect_failure':
console.log('failure while trying to resolve signInWithRedirect API.');
break;
case 'customOAuthState':
logger.info('custom state returned from CognitoHosted UI');
break;
}
});
hubListenerCancel(); // stop listening for messages
Hooks (Serverless Functions and UI Events) are not an afterthought in Gen 2; it’s been deliberately baked into the solution. This deliberate integration ensures that developers have the tools to create adaptable, scalable, and forward-looking applications. Whether enhancing the frontend with real-time interactions through Amplify Hub events or extending the backend with serverless functions, Gen 2 is designed to empower developers with the flexibility and capability to push the boundaries of modern application development.
Extend: Services
Developers prefer spending time building solutions rather than integrating new services into their applications. It takes a lot of context switching to orchestrate AWS services that needs to plug into your code. You must create and manage these services manually and you need to understand upfront the constraints of each service.
To resolve this and boost developer experience, Gen 2 is layered on top of AWS Cloud Development Kit (CDK). This means extending the resources generated by Amplify does not require any special configuration. For instance, you can create a custom stack, LocationMapStack
, using CDK to define your application’s desired Amazon Location Service resources. This stack can then be included in the amplify/backend.ts
file, ensuring it gets deployed as part of your application.
import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
import * as locations from 'aws-cdk-lib/aws-location';
import { Construct } from 'constructs';
export class LocationMapStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Create the map resource
const map = new locations.CfnMap(this, 'LocationMap', {
configuration: {
style: 'VectorEsriStreets' // map style
},
description: 'My Location Map',
mapName: 'MyMap'
});
new CfnOutput(this, 'mapArn', {
value: map.attrArn,
exportName: 'mapArn'
});
}
}
// In amplify/backend.ts
const locationStack = backend.createStack("locationStack");
new LocationMapStack(locationStack, "locationstack");
The best thing about this is you never have to leave your code editor. You can integrate Amplify’s capabilities with your existing AWS resources, allowing you to leverage your existing infrastructure alongside Amplify’s features. You also don’t need to read the service’s docs upfront since CDK is fully typed and you can get insights from Intellisense.
Ship: TTP (Time to Production)
Qovery (top-rated Internal Developer Platform), in an article on “Overcoming Shared Environment Bottlenecks,” wrote that shared development environments, once effective for collaboration, are now known for creating productivity bottlenecks. These environments often lead to conflicts and resource contention that stall projects and hinder development teams. Picture four developers working on fullstack features independently but sharing a single development environment. This shared environment becomes a source of friction and delays. As each developer makes changes to their respective components, they must carefully coordinate with their teammates to avoid conflicts and ensure that their changes don’t break the overall application.
In this scenario, Developer A eagerly updates their backend code but must wait for Developer C to complete database changes. Meanwhile, Developer B, having overhauled the UI, faces a dilemma: they need to push their code but are wary of potential conflicts with Developer D’s ongoing integration work. This shared development environment, rather than facilitating collaboration, becomes a bottleneck that hampers progress and slows down the entire development cycle.
Gen 2 introduces per-developer cloud sandbox environments. These environments, tailored for local development, deploy high-fidelity AWS backends in real-time, ensuring developers iterate on features against a production-like backdrop.
This approach revolutionizes the way teams collaborate and iterate on their cloud applications, significantly reducing development time and accelerating time to production (TTP). The simplicity of the process is equally groundbreaking; you can effortlessly deploy code to your sandbox environment by merely saving code changes. Picture those four developers seamlessly working on full-stack features independently, their environments undisturbed by each other’s changes.
Ship: Inline Backend
In traditional web development, deploying both frontend and backend components often involves a lot of configurations, multiple steps, and a potential source of friction. Inline Backend streamlines the deployment process by eliminating the need for separate frontend and backend deployments, significantly reducing the steps required to bring an application to production. This streamlined approach saves developers time and effort and minimizes the risk of deployment errors, ensuring a more reliable and consistent development experience.
With Amplify Gen 2, you can deploy your frontend and backend together on every code commit. The code-first approach ensures that the Git repository serves as the source of truth for the fullstack application’s state. All backend resources are redefined as code, promoting reproducibility and portability across branches. This, combined with central management of environment variables and secrets, simplifies the promotion workflow from lower to upper environments. Gen 2 revolutionizes fullstack application deployments by introducing a single-project deployment approach, leveraging Git branches as shared environments, embracing a code-first philosophy, and implementing centralized management of environment variables and secrets.
Ship: Add Infrastructure from TypeScript
AWS Amplify Gen 2 is taking advantage of this transition, offering a new methodology where ‘familiar-only code’ is the mantra. This approach eliminates the steep learning curve associated with traditional infrastructure management tools and languages, allowing fullstack developers to define their application infrastructure using familiar TypeScript.
Historically, setting up infrastructure required navigating through the AWS console or executing numerous CLI commands, often leading to a less predictable and potentially error-prone setup. With Gen 2, Amplify transforms this process, making infrastructure setup predictable and colocated with the code it supports, such as data, authentication, and storage services. This change addresses a significant pain point where infrastructure complexity could spiral out of control, becoming a source of unpredictability.
By authoring infrastructure as TypeScript in files following a convention (e.g., amplify/auth/resource.ts
or amplify/data/resource.ts
), developers can leverage strict typing and IntelliSense in Visual Studio Code to minimize errors. When there’s a breaking change in the backend, the immediate feedback loop provided by type errors in the frontend ensures reliability and developer confidence.
import { defineAuth } from '@aws-amplify/backend'; import secret from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { email: { verificationEmailSubject: 'Welcome, verify your email' }, externalProviders: { loginWithAmazon: { clientId: secret('LOGINWITHAMAZON_CLIENT_ID'), clientSecret: secret('LOGINWITHAMAZON_CLIENT_SECRET'), } }, }, multifactor: { mode: 'OPTIONAL', sms: { smsMessage: (code) => `Your verification code is ${code}`, }, }, userAttributes: { profilePicture: { mutable: true, required: false, }, }, });
Infrastructure from TypeScript in Gen 2 epitomizes the “convention over configuration” principle, offering a clear and organized way to manage infrastructure. It means developers can stay within the comfort of their code editor, accessing documentation on the fly and avoiding the disruption of switching contexts.
Summary
The AWS Amplify’s code-first approach is revolutionizing web development by prioritizing the customer experience and leveraging AWS services. With this approach, developers can create robust and scalable fullstack applications that meet the needs of their customers.
The key messages of this approach include the infrastructure-from-code capabilities, the ability to write code to define their infrastructure, TypeScript for fullstack development, previewing changes on every save, codegen, and zero-config fullstack deployments. By adopting these paradigms, developers can build applications using TypeScript, iterate quickly, and easily scale their applications. The AWS Amplify’s code-first approach is shaping the future of web development by providing developers with powerful tools and a seamless development experience.