🚀 API FacturaScripts with Flutter/Dart

Learn to connect your Flutter application with the FacturaScripts API

Download Complete Examples

📱 What is this API structure?

Generic API Connection for FacturaScripts

This structure provides a simple, reusable way to connect Flutter/Dart applications with FacturaScripts' REST API. It includes:

  • Centralized Configuration: URL and tokens in one place
  • Generic HTTP Services: GET, POST, PUT, DELETE predefined
  • Data Models: Automatic JSON serialization/deserialization
  • Working Example: Retrieve clients from FacturaScripts

Advantages of this Architecture

  • Reusable: Easily add new endpoints
  • Maintainable: Configuration changes in a single file
  • Scalable: Structure ready to grow
  • Type Safe: Dart models with validation
  • Error Handling: Try-catch in all methods

🔍 Explore the API Components

Select which part of the code you want to analyze in detail:

API Components

📁 Project Structure

lib/
├── config/
│   └── api_config.dart          # <?php echo t('Configuración de la API'); ?>
├── models/
│   └── cliente.dart             # <?php echo t('Modelo Cliente'); ?>
├── services/
│   ├── api_service.dart         # <?php echo t('Servicio HTTP genérico'); ?>
│   └── clientes_service.dart    # <?php echo t('Servicio de Clientes'); ?>
└── main.dart                    # <?php echo t('Ejemplo de uso'); ?>

Layered Organization: Each folder has a specific responsibility, making the code easier to maintain and scale.

⚙️ 1. Settings - api_config.dart

📄 lib/config/api_config.dart

class ApiConfig {
  static const String baseUrl = 'http://127.0.0.1';
  static const String apiPath = '/api/3';
  static const String globalApiKey = 'Dx88vjqWnUNSkDS4HFio';
  
  static String getApiUrl(String endpoint) {
    return '$baseUrl$apiPath$endpoint';
  }
  
  static Map<String, String> getHeaders({String? userApiKey}) {
    return {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      'token': userApiKey ?? globalApiKey,
    };
  }
}

🔑 Components Explained

  • baseUrl: Server address (localhost in development, real URL in production)
  • apiPath: Base path of the FacturaScripts API (/api/3 is version 3)
  • globalApiKey: Default authentication token for all requests
  • getApiUrl(): Builds complete URLs by concatenating base + path + endpoint
  • getHeaders(): Generates HTTP headers with Content-Type, Accept and authentication token

💡 Advantages of Centralizing Configuration

  • ✅ Change servers by modifying only one line
  • ✅ Change the API version in one place
  • ✅ Manage multiple tokens (global vs. user)
  • ✅ Consistent headers across all requests

📦 2. Models - cliente.dart

📄 lib/models/cliente.dart

class Cliente {
  final String codcliente;
  final String nombre;
  final String? cifnif;
  final String? email;
  final String? telefono1;
  
  Cliente({
    required this.codcliente,
    required this.nombre,
    this.cifnif,
    this.email,
    this.telefono1,
  });
  
  factory Cliente.fromJson(Map<String, dynamic> json) {
    return Cliente(
      codcliente: json['codcliente']?.toString() ?? '',
      nombre: json['nombre']?.toString() ?? '',
      cifnif: json['cifnif']?.toString(),
      email: json['email']?.toString(),
      telefono1: json['telefono1']?.toString(),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'codcliente': codcliente,
      'nombre': nombre,
      'cifnif': cifnif,
      'email': email,
      'telefono1': telefono1,
    };
  }
  
  @override
  String toString() {
    return 'Cliente(codcliente: $codcliente, nombre: $nombre, cifnif: $cifnif, email: $email, telefono1: $telefono1)';
  }
}

🏗️ Anatomy of the Model

  • Final Properties: Immutable once created (immutable pattern)
  • Nullable vs Non-nullable:
    • String - Required field
    • String? - Optional field (can be null)
  • Constructor named parameters: Clarity when instantiating objects
  • required: Forces values to be provided for non-optional fields

🔄 JSON Serialization

  • fromJson(): Converts Map<String, dynamic> (JSON) → Cliente Object
    • Use ?.toString() to handle null values
    • Operator ?? operator provides default values
  • toJson(): Converts Cliente Object → Map<String, dynamic> (JSON)
    • Useful for sending data in POST/PUT
  • toString(): Readable representation for debugging and logs

📊 Usage Example (2)

// <?php echo t('Crear desde JSON (respuesta de API)'); ?>
final jsonData = {
  'codcliente': '001',
  'nombre': 'Juan Pérez',
  'cifnif': '12345678A',
  'email': 'juan@example.com',
  'telefono1': '600123456'
};

final cliente = Cliente.fromJson(jsonData);

// <?php echo t('Convertir a JSON (para enviar a API)'); ?>
final jsonSalida = cliente.toJson();

// <?php echo t('Imprimir (usa toString automáticamente)'); ?>
print(cliente); // Cliente(codcliente: 001, nombre: Juan Pérez, ...)

🔌 3. HTTP Services - api_service.dart

📄 lib/services/api_service.dart

import 'dart:convert';
import 'package:http/http.dart' as http;
import '../config/api_config.dart';

class ApiService {
  final String? userApiKey;
  
  ApiService({this.userApiKey});
  
  /// GET genérico a cualquier endpoint
  Future<Map<String, dynamic>> get(String endpoint, {Map<String, String>? queryParams}) async {
    try {
      String url = ApiConfig.getApiUrl(endpoint);
      
      if (queryParams != null && queryParams.isNotEmpty) {
        final query = queryParams.entries
            .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
            .join('&');
        url = '$url?$query';
      }
      
      final response = await http.get(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición GET: $e');
    }
  }
  
  /// POST genérico a cualquier endpoint
  Future<Map<String, dynamic>> post(String endpoint, Map<String, dynamic> data) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.post(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
        body: json.encode(data),
      );
      
      if (response.statusCode == 200 || response.statusCode == 201) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición POST: $e');
    }
  }
  
  /// PUT genérico a cualquier endpoint
  Future<Map<String, dynamic>> put(String endpoint, Map<String, dynamic> data) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.put(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
        body: json.encode(data),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición PUT: $e');
    }
  }
  
  /// DELETE genérico a cualquier endpoint
  Future<Map<String, dynamic>> delete(String endpoint) async {
    try {
      final url = ApiConfig.getApiUrl(endpoint);
      
      final response = await http.delete(
        Uri.parse(url),
        headers: ApiConfig.getHeaders(userApiKey: userApiKey),
      );
      
      if (response.statusCode == 200) {
        return json.decode(response.body);
      } else {
        throw Exception('Error ${response.statusCode}: ${response.body}');
      }
    } catch (e) {
      throw Exception('Error en petición DELETE: $e');
    }
  }
}

🎯 GET Method - Retrieve Data

  • Purpose: Retrieve information from the server (read)
  • Query Parameters: Optional parameters for filtering results
    • Example: /clientes?nombre=Juan&activo=true
    • They're encoded with Uri.encodeComponent() for security
  • Status 200: Successful response, decode JSON
  • Try-catch: Catches network or parsing errors

📝 POST Method - Create Data

  • Purpose: Send data to the server to create resources
  • Body: Data in JSON format (converted with json.encode())
  • Status 200/201: Successful creation (201 = Created)
  • Typical Use: Create a new client, invoice, product, etc.

✏️ PUT Method - Update Data

  • Purpose: Modify an existing resource on the server
  • Difference from POST: PUT is idempotent (multiple calls = same result)
  • Typical Endpoint: /clientes/001 (includes the resource ID)

🗑️ DELETE Method - Delete Data

  • Purpose: Delete a resource from the server
  • No Body: Only needs the endpoint with the ID
  • Caution: Destructive operation, implement confirmations in the UI

📄 lib/services/clientes_service.dart

import '../models/cliente.dart';
import 'api_service.dart';

class ClientesService {
  final ApiService _apiService;
  
  ClientesService({String? userApiKey}) 
      : _apiService = ApiService(userApiKey: userApiKey);
  
  /// <?php echo t('Obtener todos los clientes'); ?>
  Future<List<Cliente>> getClientes() async {
    try {
      final response = await _apiService.get('/clientes');
      
      if (response.containsKey('clientes')) {
        final List<dynamic> clientesJson = response['clientes'];
        return clientesJson.map((json) => Cliente.fromJson(json)).toList();
      }
      
      return [];
    } catch (e) {
      throw Exception('Error al obtener clientes: $e');
    }
  }
  
  /// <?php echo t('Obtener un cliente por código'); ?>
  Future<Cliente?> getCliente(String codcliente) async {
    try {
      final response = await _apiService.get('/clientes/$codcliente');
      
      if (response.containsKey('cliente')) {
        return Cliente.fromJson(response['cliente']);
      }
      
      return null;
    } catch (e) {
      throw Exception('Error al obtener cliente: $e');
    }
  }
}

🏭 Service Layer Pattern

  • Separation of Concerns:
    • ApiService → Generic HTTP requests
    • ClientesService → Client-specific logic
  • Reusability: ApiService is used internally without exposing HTTP details
  • Strong Typing: Returns List<Cliente> instead of raw JSON
  • Error Handling: Domain-specific messages (clients)

🔍 List Deserialization

  • Step 1: response['clientes'] gets the JSON array
  • Step 2: List<dynamic> types it as a dynamic list
  • Step 3: .map((json) => Cliente.fromJson(json)) converts each element
  • Step 4: .toList() materializes the result into List<Cliente>

▶️ 4. Usage Example (2) - main.dart

📄 lib/main.dart

import 'services/clientes_service.dart';

void main() async {
  print('=== Prueba de conexión a API ===\n');
  
  // Crear instancia del servicio de clientes
  final clientesService = ClientesService();
  
  try {
    print('Obteniendo clientes...');
    final clientes = await clientesService.getClientes();
    
    print('\nTotal de clientes: ${clientes.length}\n');
    
    if (clientes.isNotEmpty) {
      print('Primeros 5 clientes:');
      for (var i = 0; i < clientes.length && i < 5; i++) {
        print('${i + 1}. ${clientes[i]}');
      }
    } else {
      print('No se encontraron clientes.');
    }
    
  } catch (e) {
    print('Error: $e');
  }
  
  print('\n=== <?php echo t('Fin de la prueba'); ?> ===');
}

🚀 Code Execution

  • void main() async: Entry point, async allows using await
  • await clientesService.getClientes(): Waits for the API response (asynchronous operation)
  • Try-catch: Catches network, parsing or API errors
  • Interpolation: \${variable} inserts values into strings

💻 How to Run the Test

From Dart terminal:
dart run lib/main.dart
From Flutter:
flutter run lib/main.dart

📊 Expected Output

=== <?php echo t('Prueba de conexión a API'); ?> ===

<?php echo t('Obteniendo clientes...'); ?>

<?php echo t('Total de clientes:'); ?> 15

<?php echo t('Primeros 5 clientes:'); ?>
1. Cliente(codcliente: 001, nombre: Juan Pérez, cifnif: 12345678A, ...)
2. Cliente(codcliente: 002, nombre: María García, cifnif: 87654321B, ...)
3. Cliente(codcliente: 003, nombre: Carlos López, cifnif: 11223344C, ...)
4. Cliente(codcliente: 004, nombre: Ana Martínez, cifnif: 55667788D, ...)
5. Cliente(codcliente: 005, nombre: Pedro Sánchez, cifnif: 99887766E, ...)

=== <?php echo t('Fin de la prueba'); ?> ===

📋 Prerequisites

1. HTTP Dependency in pubspec.yaml

dependencies:
  http: ^1.1.0

Installation: Run flutter pub get or dart pub get

2. FacturaScripts with Active API

  • URL: http://127.0.0.1 (or your server)
  • API Path: /api/3 (API version 3)
  • Token: Generate in FacturaScripts → Admin → API

3. Configure CORS (if necessary)

If you're accessing from the web or having CORS issues, configure the server to accept requests from your app's origin.

🔧 How to Extend this Structure

1️⃣ Add a New Model

Example: Producto

// lib/models/producto.dart
class Producto {
  final String referencia;
  final String descripcion;
  final double precio;
  
  Producto({
    required this.referencia,
    required this.descripcion,
    required this.precio,
  });
  
  factory Producto.fromJson(Map<String, dynamic> json) {
    return Producto(
      referencia: json['referencia'] ?? '',
      descripcion: json['descripcion'] ?? '',
      precio: (json['precio'] ?? 0.0).toDouble(),
    );
  }
  
  Map<String, dynamic> toJson() {
    return {
      'referencia': referencia,
      'descripcion': descripcion,
      'precio': precio,
    };
  }
}

2️⃣ Create a Specific Service

Example: ProductosService

// lib/services/productos_service.dart
import '../models/producto.dart';
import 'api_service.dart';

class ProductosService {
  final ApiService _apiService;
  
  ProductosService({String? userApiKey}) 
      : _apiService = ApiService(userApiKey: userApiKey);
  
  Future<List<Producto>> getProductos() async {
    final response = await _apiService.get('/productos');
    
    if (response.containsKey('productos')) {
      final List<dynamic> productosJson = response['productos'];
      return productosJson.map((json) => Producto.fromJson(json)).toList();
    }
    
    return [];
  }
  
  Future<Producto?> crearProducto(Producto producto) async {
    final response = await _apiService.post('/productos', producto.toJson());
    
    if (response.containsKey('producto')) {
      return Producto.fromJson(response['producto']);
    }
    
    return null;
  }
}

3️⃣ Use it in Your Application

Integration in Flutter

// <?php echo t('En tu Widget de Flutter'); ?>
import 'package:flutter/material.dart';
import 'services/productos_service.dart';

class ProductosPage extends StatefulWidget {
  @override
  _ProductosPageState createState() => _ProductosPageState();
}

class _ProductosPageState extends State<ProductosPage> {
  final _productosService = ProductosService();
  List<Producto> _productos = [];
  bool _cargando = true;
  
  @override
  void initState() {
    super.initState();
    _cargarProductos();
  }
  
  Future<void> _cargarProductos() async {
    try {
      final productos = await _productosService.getProductos();
      setState(() {
        _productos = productos;
        _cargando = false;
      });
    } catch (e) {
      print('Error: $e');
      setState(() => _cargando = false);
    }
  }
  
  @override
  Widget build(BuildContext context) {
    if (_cargando) {
      return Center(child: CircularProgressIndicator());
    }
    
    return ListView.builder(
      itemCount: _productos.length,
      itemBuilder: (context, index) {
        final producto = _productos[index];
        return ListTile(
          title: Text(producto.descripcion),
          subtitle: Text(producto.referencia),
          trailing: Text('${producto.precio}€'),
        );
      },
    );
  }
}

✅ Best Practices Implemented

🏗️ Architecture

  • Layer separation (config, models, services)
  • Implicit Repository pattern
  • Dependency injection (userApiKey)
  • Single Responsibility Principle

🛡️ Security and Robustness

  • Try-catch in all HTTP methods
  • Status code validation
  • Null safety with ? and ?? operators
  • URI encoding for query parameters

📦 Maintainability

  • Reusable and scalable code
  • Clear, descriptive names
  • Documentation with /// comments
  • Easy to test (async/await)

🔧 Common Troubleshooting

❌ Connection Error

Symptom: "Connection refused" or timeout

Solution:

  • Verify FacturaScripts is running
  • Check the URL in api_config.dart
  • Use a real IP on a physical device (not localhost)

🔐 Error 401 Unauthorized

Symptom: "Error 401: Unauthorized"

Solution:

  • Verify the token in globalApiKey
  • Generate a new token in FacturaScripts
  • Check that the API is enabled

🌐 CORS Error (Web)

Symptom: "CORS policy blocked"

Solution:

  • Configure CORS headers on the server
  • Use a proxy during development
  • In production, configure the allowed domain

📦 JSON Parsing Error

Symptom: "type 'Null' is not a subtype of..."

Solution:

  • Add ?.toString() validation
  • Use the ?? operator for default values
  • Print the raw response for debugging

⚡ HTTP Dependency Not Found

Symptom: "Error: Cannot find package 'http'"

Solution:

  • Add http: ^1.1.0 to pubspec.yaml
  • Run flutter pub get
  • Restart the IDE if necessary

🐛 Empty List Without Errors

Symptom: clientes.length == 0 but there's no error

Solution:

  • Verify the JSON key: response['clientes']
  • Print response to see the structure
  • Check that there is data in FacturaScripts

🚀 Next Steps

1. Add More Models

Create models for other FacturaScripts resources:

  • Invoices (factura.dart)
  • Products (producto.dart)
  • Articles (articulo.dart)
  • Suppliers (proveedor.dart)

2. Implement Complete CRUD

Add CREATE, UPDATE, DELETE operations to your services:

  • crearCliente() using POST
  • actualizarCliente() using PUT
  • eliminarCliente() using DELETE

3. Improve State Management

Integrate with Flutter state management solutions:

  • Provider for simple management
  • Riverpod for a more robust architecture
  • BLoC for complex applications

4. Add Caching and Persistence

Optimize performance and the offline experience:

  • SharedPreferences for simple data
  • Hive or SQLite for structured data
  • cache-first or network-first strategy