flutter
Uncategorized

Flutter CRUD App Using Fake JSON Server (Complete Step-by-Step Guide)

Flutter CRUD Application Using Fake JSON Server

In this blog, we will build a complete CRUD (Create, Read, Update, Delete)
application using Flutter and a Fake JSON Server. This approach is ideal for
learning API integration without setting up a real backend.

We will cover project setup, fake backend creation, model design, API services,
UI screens, and common real-world issues faced during development.

 


⭐ What is JSON Server?

JSON Server is a lightweight tool that allows you to create a fake REST API
using a simple JSON file. It supports GET, POST, PUT, PATCH, and DELETE requests
out of the box.


flutter_json_crud/
│
├── db.json
│
├── pubspec.yaml
│
└── lib/
    │
    ├── main.dart
    │
    ├── models/
    │   └── user_model.dart
    │
    ├── services/
    │   └── user_service.dart
    │
    └── screens/
        ├── user_list_screen.dart
        └── user_form_screen.dart

📌 Step 1: Create Fake Backend (db.json)

Create a file named db.json in your project directory and add
the following content:

{
  "users": [
    {
      "id": "1",
      "name": "Umar Rahman",
      "email": "umar@gmail.com",
      "phone": "9999999999"
    }
  ]
}

Start the JSON server using the command below:

npx json-server --watch db.json --port 3000 --cors

Test the API in your browser:

http://localhost:3000/users

📌 Step 2: Flutter Project Setup

Create a new Flutter project and add the HTTP package to
pubspec.yaml.

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.0

📌 Step 3: User Model

Create a model class to map API data. IDs are treated as strings because
JSON Server may generate alphanumeric IDs.


class User {
  final String? id;
  final String name;
  final String email;
  final String phone;

  User({
    this.id,
    required this.name,
    required this.email,
    required this.phone,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id']?.toString(), // SAFE for string / UUID ids
      name: json['name'],
      email: json['email'],
      phone: json['phone'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "email": email,
      "phone": phone,
    };
  }
}

📌 Step 4: API Service (CRUD Operations)

The service class handles all communication with the fake backend.


import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/user_model.dart';

class UserService {
  static const String baseUrl = "http://localhost:3000/users";

  // READ
  static Future<List<User>> fetchUsers() async {
    final response = await http.get(Uri.parse(baseUrl));
    final List data = jsonDecode(response.body);
    return data.map((e) => User.fromJson(e)).toList();
  }

  // CREATE
  static Future<void> addUser(User user) async {
    await http.post(
      Uri.parse(baseUrl),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode(user.toJson()),
    );
  }

  // UPDATE  (id as String – safe for UUID / alphanumeric ids)
  static Future<void> updateUser(String id, User user) async {
    await http.put(
      Uri.parse("$baseUrl/$id"),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode(user.toJson()),
    );
  }

  // DELETE  (id as String)
  static Future<void> deleteUser(String id) async {
    await http.delete(Uri.parse("$baseUrl/$id"));
  }
}

📌 Step 5: User List Screen (Read + Delete)

This screen displays users from the API and allows edit and delete actions.


import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/user_service.dart';
import 'user_form_screen.dart';

class UserListScreen extends StatefulWidget {
  const UserListScreen({super.key});

  @override
  State<UserListScreen> createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  late Future<List<User>> users;

  @override
  void initState() {
    super.initState();
    users = UserService.fetchUsers();
  }

  void refresh() {
    setState(() {
      users = UserService.fetchUsers();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Users CRUD")),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: () async {
          final result = await Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => const UserFormScreen()),
          );

          if (result == true) {
            refresh(); // refresh only on success
          }
        },
      ),
      body: FutureBuilder<List<User>>(
        future: users,
        builder: (context, snapshot) {
          // Loading
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }

          // Error
          if (snapshot.hasError) {
            return Center(
              child: Text(
                "Error: ${snapshot.error}",
                style: const TextStyle(color: Colors.red),
              ),
            );
          }

          // No data
          if (!snapshot.hasData || snapshot.data!.isEmpty) {
            return const Center(child: Text("No users found"));
          }

          final userList = snapshot.data!;

          return ListView.builder(
            itemCount: userList.length,
            itemBuilder: (context, index) {
              final user = userList[index];

              return Card(
                margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
                child: ListTile(
                  title: Text(user.name),
                  subtitle: Text(user.email),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      // EDIT
                      IconButton(
                        icon: const Icon(Icons.edit),
                        onPressed: () async {
                          final result = await Navigator.push(
                            context,
                            MaterialPageRoute(
                              builder: (_) => UserFormScreen(user: user),
                            ),
                          );

                          if (result == true) {
                            refresh();
                          }
                        },
                      ),

                      // DELETE
                      IconButton(
                        icon: const Icon(Icons.delete, color: Colors.red),
                        onPressed: () async {
                          await UserService.deleteUser(user.id!);
                          refresh();
                        },
                      ),
                    ],
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

📌 Step 6: Add User Screen


import 'package:flutter/material.dart';
import '../models/user_model.dart';
import '../services/user_service.dart';

class UserFormScreen extends StatefulWidget {
  final User? user;
  const UserFormScreen({super.key, this.user});

  @override
  State<UserFormScreen> createState() => _UserFormScreenState();
}

class _UserFormScreenState extends State<UserFormScreen> {
  final _formKey = GlobalKey<FormState>();

  late TextEditingController name;
  late TextEditingController email;
  late TextEditingController phone;

  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    name = TextEditingController(text: widget.user?.name ?? "");
    email = TextEditingController(text: widget.user?.email ?? "");
    phone = TextEditingController(text: widget.user?.phone ?? "");
  }

  @override
  void dispose() {
    name.dispose();
    email.dispose();
    phone.dispose();
    super.dispose();
  }

  Future<void> saveUser() async {
    if (!_formKey.currentState!.validate()) return;

    setState(() => isLoading = true);

    final user = User(
      name: name.text.trim(),
      email: email.text.trim(),
      phone: phone.text.trim(),
    );

    try {
      if (widget.user == null) {
        await UserService.addUser(user);
      } else {
        await UserService.updateUser(widget.user!.id!, user);
      }

      Navigator.pop(context, true); // return success
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("Error: $e")),
      );
    } finally {
      if (mounted) {
        setState(() => isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.user == null ? "Add User" : "Update User"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: name,
                decoration: const InputDecoration(labelText: "Name"),
                validator: (v) =>
                    v == null || v.isEmpty ? "Name required" : null,
              ),
              TextFormField(
                controller: email,
                decoration: const InputDecoration(labelText: "Email"),
                validator: (v) =>
                    v == null || v.isEmpty ? "Email required" : null,
              ),
              TextFormField(
                controller: phone,
                decoration: const InputDecoration(labelText: "Phone"),
                validator: (v) =>
                    v == null || v.isEmpty ? "Phone required" : null,
              ),
              const SizedBox(height: 20),
              ElevatedButton(
                onPressed: isLoading ? null : saveUser,
                child: isLoading
                    ? const CircularProgressIndicator(color: Colors.white)
                    : const Text("Save"),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

 

📌 Step 7: Main


import 'package:flutter/material.dart';
import 'screens/user_list_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: UserListScreen(),
    );
  }
}

📌 Step 8: Output

 

 


⭐ Common Issues and Fixes

  • ID format mismatch (always treat ID as String)
  • Infinite loader due to missing FutureBuilder states
  • List not refreshing after navigation
  • Missing CORS in Flutter Web

📌 Final Output

  • Fake REST API using JSON Server
  • Flutter CRUD operations
  • Clean architecture (Model, Service, UI)
  • Web and mobile compatible

📌 Summary

This blog demonstrated how to build a complete Flutter CRUD application using a
Fake JSON Server. This setup is perfect for beginners and mirrors real-world API
integration patterns used in production applications.

Code at Github

Leave a Reply

Your email address will not be published. Required fields are marked *