🚀 API FacturaScripts con Flutter/Dart
Aprende a conectar tu aplicación Flutter con la API de FacturaScripts
Descargar Ejemplos Completos📱 ¿Qué es esta estructura de API?
Conexión API Genérica para FacturaScripts
Esta estructura proporciona una forma simple y reutilizable de conectar aplicaciones Flutter/Dart con la API REST de FacturaScripts. Incluye:
- Configuración centralizada: URL y tokens en un solo lugar
- Servicios HTTP genéricos: GET, POST, PUT, DELETE predefinidos
- Modelos de datos: Serialización/deserialización JSON automática
- Ejemplo funcional: Obtener clientes desde FacturaScripts
Ventajas de esta arquitectura
- ✅ Reutilizable: Añade nuevos endpoints fácilmente
- ✅ Mantenible: Cambios de configuración en un solo archivo
- ✅ Escalable: Estructura preparada para crecer
- ✅ Tipado seguro: Modelos Dart con validación
- ✅ Gestión de errores: Try-catch en todos los métodos
🔍 Explora los Componentes de la API
Selecciona qué parte del código quieres analizar en detalle:
Componentes de la API
📁 Estructura del Proyecto
lib/
├── config/
│ └── api_config.dart # Configuración de la API
├── models/
│ └── cliente.dart # Modelo Cliente
├── services/
│ ├── api_service.dart # Servicio HTTP genérico
│ └── clientes_service.dart # Servicio de Clientes
└── main.dart # Ejemplo de uso
Organización por capas: Cada carpeta tiene una responsabilidad específica, facilitando el mantenimiento y la escalabilidad del código.
⚙️ 1. Configuración - 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,
};
}
}
🔑 Componentes Explicados
- baseUrl: Dirección del servidor (localhost en desarrollo, URL real en producción)
- apiPath: Ruta base de la API de FacturaScripts (/api/3 es la versión 3)
- globalApiKey: Token de autenticación predeterminado para todas las peticiones
- getApiUrl(): Construye URLs completas concatenando base + path + endpoint
- getHeaders(): Genera headers HTTP con Content-Type, Accept y token de autenticación
💡 Ventajas de Centralizar la Configuración
- ✅ Cambiar de servidor modificando solo una línea
- ✅ Cambiar versión de API en un solo lugar
- ✅ Gestionar múltiples tokens (global vs usuario)
- ✅ Headers consistentes en todas las peticiones
📦 2. Modelos - 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)';
}
}
🏗️ Anatomía del Modelo
- Propiedades final: Inmutables una vez creadas (patrón inmutable)
- Nullable vs Non-nullable:
String- Campo obligatorioString?- Campo opcional (puede ser null)
- Constructor named parameters: Claridad al instanciar objetos
- required: Obliga a proporcionar valores para campos no opcionales
🔄 Serialización JSON
- fromJson(): Convierte Map<String, dynamic> (JSON) → Objeto Cliente
- Usa
?.toString()para manejar valores null - Operador
??proporciona valores por defecto
- Usa
- toJson(): Convierte Objeto Cliente → Map<String, dynamic> (JSON)
- Útil para enviar datos en POST/PUT
- toString(): Representación legible para debugging y logs
📊 Ejemplo de Uso
// 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);
// Convertir a JSON (para enviar a API)
final jsonSalida = cliente.toJson();
// Imprimir (usa toString automáticamente)
print(cliente); // Cliente(codcliente: 001, nombre: Juan Pérez, ...)
🔌 3. Servicios HTTP - 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');
}
}
}
🎯 Método GET - Obtener Datos
- Propósito: Recuperar información del servidor (lectura)
- Query Parameters: Parámetros opcionales para filtrar resultados
- Ejemplo:
/clientes?nombre=Juan&activo=true - Se codifican con
Uri.encodeComponent()para seguridad
- Ejemplo:
- Status 200: Respuesta exitosa, decodifica JSON
- Try-catch: Captura errores de red o parsing
📝 Método POST - Crear Datos
- Propósito: Enviar datos al servidor para crear recursos
- Body: Datos en formato JSON (convertidos con
json.encode()) - Status 200/201: Creación exitosa (201 = Created)
- Uso típico: Crear nuevo cliente, factura, producto, etc.
✏️ Método PUT - Actualizar Datos
- Propósito: Modificar un recurso existente en el servidor
- Diferencia con POST: PUT es idempotente (múltiples llamadas = mismo resultado)
- Endpoint típico:
/clientes/001(incluye ID del recurso)
🗑️ Método DELETE - Eliminar Datos
- Propósito: Borrar un recurso del servidor
- Sin body: Solo necesita el endpoint con el ID
- Precaución: Operación destructiva, implementar confirmaciones en 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);
/// 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');
}
}
/// 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');
}
}
}
🏭 Patrón Service Layer
- Separación de responsabilidades:
ApiService→ Peticiones HTTP genéricasClientesService→ Lógica específica de Clientes
- Reutilización: ApiService se usa internamente sin exponer detalles HTTP
- Tipado fuerte: Devuelve
List<Cliente>en vez de JSON crudo - Gestión de errores: Mensajes específicos del dominio (clientes)
🔍 Deserialización de Lista
- Paso 1:
response['clientes']obtiene el array JSON - Paso 2:
List<dynamic>lo tipa como lista dinámica - Paso 3:
.map((json) => Cliente.fromJson(json))convierte cada elemento - Paso 4:
.toList()materializa el resultado en List<Cliente>
▶️ 4. Ejemplo de Uso - 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=== Fin de la prueba ===');
}
🚀 Ejecución del Código
- void main() async: Punto de entrada,
asyncpermite usarawait - await clientesService.getClientes(): Espera la respuesta de la API (operación asíncrona)
- Try-catch: Captura errores de red, parsing o API
- Interpolación:
\${variable}inserta valores en strings
💻 Cómo Ejecutar la Prueba
Desde terminal Dart:
dart run lib/main.dart
Desde Flutter:
flutter run lib/main.dart
📊 Salida Esperada
=== Prueba de conexión a API ===
Obteniendo clientes...
Total de clientes: 15
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, ...)
=== Fin de la prueba ===
📋 Requisitos Previos
1. Dependencia HTTP en pubspec.yaml
dependencies:
http: ^1.1.0
Instalación: Ejecuta flutter pub get o dart pub get
2. FacturaScripts con API Activa
- URL: http://127.0.0.1 (o tu servidor)
- API Path: /api/3 (versión 3 de la API)
- Token: Generar en FacturaScripts → Administrador → API
3. Configurar CORS (si es necesario)
Si accedes desde web o tienes problemas de CORS, configura el servidor para aceptar peticiones del origen de tu app.
🔧 Cómo Extender esta Estructura
1️⃣ Añadir Nuevo Modelo
Ejemplo: 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️⃣ Crear Servicio Específico
Ejemplo: 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️⃣ Usar en Tu Aplicación
Integración en Flutter
// 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}€'),
);
},
);
}
}
✅ Mejores Prácticas Implementadas
🏗️ Arquitectura
- Separación de capas (config, models, services)
- Patrón Repository implícito
- Inyección de dependencias (userApiKey)
- Single Responsibility Principle
🛡️ Seguridad y Robustez
- Try-catch en todos los métodos HTTP
- Validación de status codes
- Null safety con operadores
?y?? - URI encoding para query parameters
📦 Mantenibilidad
- Código reutilizable y escalable
- Nombres descriptivos y claros
- Documentación con comentarios
/// - Fácil de testear (async/await)
🔧 Solución de Problemas Comunes
❌ Error de Conexión
Síntoma: "Connection refused" o timeout
Solución:
- Verifica que FacturaScripts esté ejecutándose
- Comprueba la URL en
api_config.dart - Usa IP real en dispositivo físico (no localhost)
🔐 Error 401 Unauthorized
Síntoma: "Error 401: Unauthorized"
Solución:
- Verifica el token en
globalApiKey - Genera un nuevo token en FacturaScripts
- Comprueba que la API esté habilitada
🌐 Error CORS (Web)
Síntoma: "CORS policy blocked"
Solución:
- Configura headers CORS en el servidor
- Usa proxy durante desarrollo
- En producción, configura dominio permitido
📦 Error de Parsing JSON
Síntoma: "type 'Null' is not a subtype of..."
Solución:
- Agrega validación
?.toString() - Usa operador
??para valores por defecto - Imprime la respuesta cruda para debug
⚡ Dependencia HTTP no Encontrada
Síntoma: "Error: Cannot find package 'http'"
Solución:
- Añade
http: ^1.1.0a pubspec.yaml - Ejecuta
flutter pub get - Reinicia el IDE si es necesario
🐛 Lista Vacía sin Errores
Síntoma: clientes.length == 0 pero no hay error
Solución:
- Verifica la clave JSON:
response['clientes'] - Imprime
responsepara ver estructura - Comprueba que hay datos en FacturaScripts
🚀 Próximos Pasos
1. Añadir Más Modelos
Crea modelos para otros recursos de FacturaScripts:
- Facturas (
factura.dart) - Productos (
producto.dart) - Artículos (
articulo.dart) - Proveedores (
proveedor.dart)
2. Implementar CRUD Completo
Añade operaciones CREATE, UPDATE, DELETE a tus servicios:
crearCliente()usando POSTactualizarCliente()usando PUTeliminarCliente()usando DELETE
3. Mejorar Gestión de Estados
Integra con soluciones de estado de Flutter:
- Provider para gestión simple
- Riverpod para arquitectura más robusta
- BLoC para aplicaciones complejas
4. Añadir Caché y Persistencia
Optimiza el rendimiento y experiencia offline:
- SharedPreferences para datos simples
- Hive o SQLite para datos estructurados
- Estrategia cache-first o network-first