How to fetch data from an API in Flutter using SpaceX API
Making a connection between an API and your Flutter application can be done with the help of packages. These packages will make sure that we can send HTTP requests to an API and deserialize the JSON data.
During this tutorial, we will be fetching rocket launch data from the SpaceX API.
1. New project
Let us start by creating a new project flutter create flutter_deserialize_json_response
. After our new project is created we want to create the necessary directories. Make sure to create a directory for models
and pages
inside the lib
directory.
flutter_deserialize_json_response
|-- lib/
|-- models/
|-- pages/
The next step is to modify the pubspec.yaml
file to include all the packages we need.
name: flutter_deserialize_json_response
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: '>=2.19.6 <3.0.0'
dependencies:
flutter:
sdk: flutter
http: ^0.13.5
json_annotation: ^4.8.0
mocktail: ^0.3.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.3.3
flutter_lints: ^2.0.0
json_serializable: ^6.6.1
test: ^1.22.0
flutter:
uses-material-design: true
When working with.yaml
files it is very important to pay attention to the indentation. If the indentation is not correct the.yaml
file will not work correctly.
Let us go over the packages:
- http to make HTTP requests;
- json_serializable and json_annotation are needed for handling JSON;
- build_runner is used to generate our JSON deserialization functions;
- mocktail will be used to Mock our classes in tests.
After we modified our pubspec.yaml
let us run flutter pub get
to install all the packages.
1.1 Model
Now that we have all the packages installed let us create our Launch model. From the SpaceX API we want to retrieve the launches. Whether the API returns a list of launches or just a single launch, we always want to make sure that it will be deserialized into our Launch model.
We want to create a launch.dart
file inside our models
directory, with the following content:
import 'package:json_annotation/json_annotation.dart';
part 'launch.g.dart';
@JsonSerializable()
class Launch {
const Launch({
required this.flightNumber,
required this.name,
this.success,
});
factory Launch.fromJson(Map<String, dynamic> json) =>
_$LaunchFromJson(json);
final int flightNumber;
final String name;
final bool? success;
}
As you can see on line 3 we refer to part 'launch.g.dart'
this file will be generated and will contain the _$LaunchFromJson
function.
Before we generate our launch.g.dart
file using build_runner, let us create a build.yaml
file inside our main directory. The build_runner package will use the configuration inside our build.yaml
file.
Here is the content of our build.yaml
file:
targets:
$default:
builders:
json_serializable:
options:
field_rename: snake
create_to_json: false
checked: true
The reason we created a build.yaml
file is to avoid creating a toJson
function because the json_serializable package can also be used to serialize a Dart object into JSON.
Also if we take a look at the data we receive from the SpaceX API you can see that they use snake_case for their JSON attributes. We want to convert them into pascalCase to follow the Dart convention. For example, we want to save the flight_number
as flightNumber
inside our model.
Now that our build.yaml
is created we can execute flutter pub run build_runner build
to create our launch.g.dart
file.
1.2 Client
To fetch data from the API we create a separate client class called LaunchClient. We want to create a launch_client.dart
file inside the lib
directory with the following content:
import 'dart:convert';
import 'package:flutter_deserialize_json_response/models/launch.dart';
import 'package:http/http.dart' as http;
class LaunchClient {
LaunchClient({http.Client? httpClient})
: _httpClient = httpClient ?? http.Client();
final http.Client _httpClient;
Future<Launch> getLaunch() async {
final request = Uri.https('api.spacexdata.com', 'v5/launches/latest');
final response = await _httpClient.get(request);
final bodyJson = jsonDecode(response.body) as Map<String, dynamic>;
return Launch.fromJson(bodyJson);
}
Future<List<Launch>> getLaunches() async {
final request = Uri.https('api.spacexdata.com', 'v5/launches');
final response = await _httpClient.get(request);
final bodyJson = jsonDecode(response.body) as List;
return bodyJson
.map((json) => Launch.fromJson(json as Map<String, dynamic>))
.toList();
}
}
As you can see we have two functions:
- getLaunch which deserializes a JSON object into a Launch object;
- getLaunches which deserializes a JSON list of objects into a list of Launch objects.
1.3 User interface
After we created our client we can create the page that will show the data. We want to create this file inside the pages
directory and name it launch_page.dart
. Here is our launch_page.dart
file:
import 'package:flutter/material.dart';
import 'package:flutter_deserialize_json_response/launch_client.dart';
import 'package:flutter_deserialize_json_response/models/launch.dart';
class LaunchPage extends StatefulWidget {
const LaunchPage({super.key});
@override
State<LaunchPage> createState() => _LaunchPageState();
}
class _LaunchPageState extends State<LaunchPage> {
Launch _launch = const Launch(
name: 'name',
flightNumber: 0,
success: true,
);
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final LaunchClient launchClient = LaunchClient();
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_launch.name, style: textTheme.headlineSmall),
Text(_launch.flightNumber.toString(), style: textTheme.headlineSmall),
Text(_launch.success.toString(), style: textTheme.headlineSmall),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () async => await launchClient.getLaunch()
.then((launch) => setState(() => _launch = launch)),
child: const Text('getLaunch'),
),
ElevatedButton(
onPressed: () async => await launchClient.getLaunches()
.then((launches) => setState(() => _launch = launches.first)),
child: const Text('getLaunches'),
)
],
)
],
);
}
}
Now that we have our page let us make sure that it is shown. We can do this by making changes in our main.dart
file. Our main.dart
file looks like this:
import 'package:flutter/material.dart';
import 'package:flutter_deserialize_json_response/pages/launch_page.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Scaffold(body: LaunchPage()),
);
}
}
Now that we have everything our directories and Dart files look like this:
flutter_deserialize_json_response
|-- lib/
|-- models/
|-- launch.dart
|-- launch.g.dart
|-- pages/
|-- launch_page.dart
|-- launch_client.dart
|-- main.dart
If you have the same files, you can start your emulator and run main.dart
.
When the application is started you will see that we have 2 buttons and 3 Text widgets that display launch data.

When your press either getLaunch or getLaunches you will see that the data changes.

In the getLaunch function, we deserialize a single JSON object into our Launch model. In the getLaunches function we deserialize a list of JSON objects into a list of Launch models and show the first one.
As you can see we successfully fetched data from the SpaceX API. To make sure that the deserialization works as intended we will add 2 tests.
1.4 Testing
It is always a good idea to add tests, this will make our application more robust and makes sure that we do not break anything when implementing changes in the future.
Inside the test
directory we want to add a launch_client_test.dart
file with the following content:
import 'package:flutter_deserialize_json_response/launch_client.dart';
import 'package:flutter_deserialize_json_response/models/launch.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
class MockHttpClient extends Mock implements http.Client {}
class MockResponse extends Mock implements http.Response {}
class FakeUri extends Fake implements Uri {}
void main() {
late http.Client httpClient;
late LaunchClient launchClient;
setUpAll(() {
registerFallbackValue(FakeUri());
});
setUp(() {
httpClient = MockHttpClient();
launchClient = LaunchClient(httpClient: httpClient);
});
group('launch', () {
test('returns a launch', () async {
final response = MockResponse();
when(() => response.statusCode).thenReturn(200);
when(() => response.body).thenReturn('''
{
"flight_number": 1,
"name": "first launch",
"success": false
}
''');
when(() => httpClient.get(any())).thenAnswer((_) async => response);
final actual = await launchClient.getLaunch();
expect(
actual,
isA<Launch>()
.having((l) => l.flightNumber, 'flightNumber', 1)
.having((l) => l.name, 'name', 'first launch')
.having((l) => l.success, 'success', false),
);
});
test('returns multiple launches', () async {
final response = MockResponse();
when(() => response.statusCode).thenReturn(200);
when(() => response.body).thenReturn('''
[
{
"flight_number": 1,
"name": "first launch",
"success": false
},
{
"flight_number": 2,
"name": "second launch",
"success": true
}
]
''');
when(() => httpClient.get(any())).thenAnswer((_) async => response);
final actual = await launchClient.getLaunches();
expect(
actual.first,
isA<Launch>()
.having((l) => l.flightNumber, 'flightNumber', 1)
.having((l) => l.name, 'name', 'first launch')
.having((l) => l.success, 'success', false),
);
expect(actual, isA<List<Launch>>());
});
});
}
These tests make sure that we get the expected response with the provided JSON data.
You can see that in the first "launch" test "returns a launch" we are testing with a single JSON object:
{
"flight_number": 1,
"name": "first launch",
"success": false
}
In the second "launch" test "returns multiple launches" we are testing with a JSON list of objects:
[
{
"flight_number": 1,
"name": "first launch",
"success": false
},
{
"flight_number": 2,
"name": "second launch",
"success": true
}
]
If we run both tests by executing flutter test
you will see that both tests are successful.

With the tests we reached the end of the tutorial, I hope you learned something!
2. Conclusion
In this tutorial, you learned how to connect a Flutter application to an API using the http package. You also learned how to deserialize JSON responses using the json_serializable package and test them. If you enjoyed using these packages, make sure you like them on pub.dev and star them on GitHub.
The source code of this tutorial can be found here: flutter_deserialize_json_response.