🚀 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 fieldString?- 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
- Use
- 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
- Example:
- 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 requestsClientesService→ 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,
asyncallows usingawait - 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.0to 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
responseto 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 POSTactualizarCliente()using PUTeliminarCliente()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