Amplify has re-imagined the way frontend developers build fullstack applications. Develop and deploy without the hassle.

Page updated May 21, 2024

Set up Amplify Data

In this guide, you will learn how to set up Amplify Data. This includes building a real-time API and database using TypeScript to define your data model, and securing your API with authorization rules. We will also explore using AWS Lambda to scale to custom use cases.

Before you begin, you will need:

  • Node.js v18.16.0 or later
  • npm v6.14.4 or later
  • git v2.14.1 or later

With Amplify Data, you can build a secure, real-time API backed by a database in minutes. After you define your data model using TypeScript, Amplify will deploy a real-time API for you. This API is powered by AWS AppSync and connected to an Amazon DynamoDB database. You can secure your API with authorization rules and scale to custom use cases with AWS Lambda.

Building your data backend

If you've run npm create amplify@latest already, you should see an amplify/data/resource.ts file, which is the central location to configure your data backend. The most important element is the schema object, which defines your backend data models (a.model()) and custom queries (a.query()), mutations (a.mutation()), and subscriptions (a.subscription()).

amplify/data/resource.ts
import { a, defineData, type ClientSchema } from '@aws-amplify/backend';
const schema = a.schema({
Todo: a.model({
content: a.string(),
isDone: a.boolean()
})
.authorization(allow => [allow.publicApiKey()])
});
// Used for code completion / highlighting when making requests from frontend
export type Schema = ClientSchema<typeof schema>;
// defines the data resource to be deployed
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'apiKey',
apiKeyAuthorizationMode: { expiresInDays: 30 }
}
});

Every a.model() automatically creates the following resources in the cloud:

  • a DynamoDB database table to store records
  • query and mutation APIs to create, read (list/get), update, and delete records
  • createdAt and updatedAt fields that help you keep track of when each record was initially created or when it was last updated
  • real-time APIs to subscribe for create, update, and delete events of records

The allow.publicApiKey() rule designates that anyone authenticated using an API key can create, read, update, and delete todos.

To deploy these resources to your cloud sandbox, run the following CLI command in your terminal:

Terminal
npx ampx sandbox --outputs-format dart --outputs-out-dir lib

Connect your application code to the data backend

Once the cloud sandbox is up and running, it will also create an amplify_outputs.json file, which includes the relevant connection information to your data backend, like your API endpoint URL and API key.

To connect your frontend code to your backend, you need to:

  1. Configure the Amplify library with the Amplify client configuration file (amplify_outputs.json)
  2. Generate a new API client from the Amplify library
  3. Make an API request with end-to-end type-safety

From your project root directory, find and modify your pubspec.yaml and add the Amplify plugins to the project dependencies.

pubspec.yaml
dependencies:
amplify_api: ^2.0.0
amplify_flutter: ^2.0.0
flutter:
sdk: flutter

Install the dependencies by running the following command. Depending on your development environment, you may perform this step via your IDE (or it may even be performed for you automatically).

Terminal
flutter pub get

Now, let's generate the GraphQL client code for your Flutter application. Amplify Data uses GraphQL under the hood to make query, mutation, and subscription requests. The generated GraphQL client code helps you to author fully-typed API requests without needing to hand-author GraphQL requests and manually map them to Dart code.

Terminal
npx ampx generate graphql-client-code --format modelgen --model-target dart --out <path_to_flutter_project>/lib/models

Finally, let's add the necessary plugins into the Flutter application by customizing the main() function of the lib/main.dart file:

lib/main.dart
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'amplifyconfiguration.dart';
import 'models/ModelProvider.dart';
Future<void> main() async {
try {
final api = AmplifyAPI(modelProvider: ModelProvider.instance);
await Amplify.addPlugins([api]);
await Amplify.configure(outputs);
safePrint('Successfully configured Amplify');
} on Exception catch (e) {
safePrint('Error configuring Amplify: $e');
}
runApp(const MyApp());
}

Write data to your backend

In your page, let's add a floating action button that creates a new todo.

lib/main.dart
// ... main()
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Your todos',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newTodo = Todo(content: "New Flutter todo", isDone: false);
final request = ModelMutations.create(newTodo);
final response = await Amplify.API.mutate(request: request).response;
if (response.hasErrors) {
safePrint('Creating Todo failed.');
} else {
safePrint('Creating Todo successful.');
}
},
tooltip: 'Add todo',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

Now if you run the application, and click on the floating action button, you should see a log indicating a todo was created:

Creating Todo successful.

Read data from your backend

Next, list all your todos and then refetch the todos after a todo has been added:

Start by adding a new list to track the todos and the ability to fetch the todo list when it first renders:

lib/main.dart
// ...main()
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Todo> _todos = [];
void initState() {
super.initState();
_refreshTodos();
}
Future<void> _refreshTodos() async {
try {
final request = ModelQueries.list(Todo.classType);
final response = await Amplify.API.query(request: request).response;
final todos = response.data?.items;
if (response.hasErrors) {
safePrint('errors: ${response.errors}');
return;
}
setState(() {
safePrint(todos);
_todos = todos!.whereType<Todo>().toList();
});
} on ApiException catch (e) {
safePrint('Query failed: $e');
}
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Your todos',
),
_todos.isEmpty == true
? const Center(
child: Text(
"The list is empty.\nAdd some items by clicking the floating action button.",
textAlign: TextAlign.center,
),
)
: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: _todos.length,
itemBuilder: (context, index) {
final todo = _todos[index];
return CheckboxListTile.adaptive(
value: todo.isDone,
title: Text(todo.content!),
onChanged: (isChecked) async {},
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final newTodo = Todo(content: "New Flutter todo", isDone: false);
final request = ModelMutations.create(newTodo);
final response = await Amplify.API.mutate(request: request).response;
if (response.hasErrors) {
safePrint('Creating Todo failed.');
} else {
safePrint('Creating Todo successful.');
}
},
tooltip: 'Add todo',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}

Subscribe to real-time updates

To add real-time updates, you can use the subscription feature of Amplify Data. It allows to subscribe to onCreate, onUpdate, and onDelete events of the application. In our example, let's append the list every time a new todo is added.

When the page renders, subscribe to onCreate events and then unsubscribe when the Widget is disposed.

lib/main.dart
// ...main()
// ...MyApp
// ...MyHomePage
class _MyHomePageState extends State<MyHomePage> {
List<Todo> _todos = [];
StreamSubscription<GraphQLResponse<Todo>>? subscription;
void initState() {
super.initState();
_refreshTodos();
_subscribe();
}
void dispose() {
_unsubscribe();
super.dispose();
}
void _subscribe() {
final subscriptionRequest = ModelSubscriptions.onCreate(Todo.classType);
final Stream<GraphQLResponse<Todo>> operation = Amplify.API.subscribe(
subscriptionRequest,
onEstablished: () => safePrint('Subscription established'),
);
subscription = operation.listen(
(event) {
safePrint('Subscription event data received: ${event.data}');
setState(() {
_todos.add(event.data!);
});
},
onError: (Object e) => safePrint('Error in subscription stream: $e'),
);
}
void _unsubscribe() {
subscription?.cancel();
subscription = null;
}
// ..._refreshTodos()
// ...build()
}

Conclusion

Success! You've learned how to create your first real-time API and database with Amplify Data.

Next steps

There's so much more to discover with Amplify Data. Learn more about: