Crea tu Primera API REST con Flask: Tutorial Completo Paso a Paso

👤 Admin 📅 30 de octubre, 2025 ⏱ 20 min 🏷 Desarrollo Web

¿Qué es una API REST?

Una API REST (Representational State Transfer) es un servicio web que permite la comunicación entre aplicaciones usando el protocolo HTTP. Flask es perfecto para crear APIs ligeras y eficientes.

Instalación y Setup

# Crear entorno virtual
python -m venv venv

# Activar entorno virtual
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate

# Instalar Flask
pip install flask
pip install flask-cors  # Para CORS

# Opcional: para base de datos
pip install flask-sqlalchemy

API Básica con Flask

# app.py
from flask import Flask, jsonify, request

app = Flask(__name__)

# Datos de ejemplo (en memoria)
usuarios = [
    {'id': 1, 'nombre': 'Ana', 'email': 'ana@email.com'},
    {'id': 2, 'nombre': 'Luis', 'email': 'luis@email.com'}
]

@app.route('/')
def home():
    return jsonify({'mensaje': 'Bienvenido a mi API'})

if __name__ == '__main__':
    app.run(debug=True)

Métodos HTTP - CRUD Completo

GET - Obtener Datos

# Obtener todos los usuarios
@app.route('/api/usuarios', methods=['GET'])
def obtener_usuarios():
    return jsonify({
        'usuarios': usuarios,
        'total': len(usuarios)
    }), 200

# Obtener usuario específico
@app.route('/api/usuarios/<int:id>', methods=['GET'])
def obtener_usuario(id):
    usuario = next((u for u in usuarios if u['id'] == id), None)
    
    if usuario:
        return jsonify(usuario), 200
    else:
        return jsonify({'error': 'Usuario no encontrado'}), 404

POST - Crear Datos

# Crear nuevo usuario
@app.route('/api/usuarios', methods=['POST'])
def crear_usuario():
    # Obtener datos del request
    datos = request.get_json()
    
    # Validar datos
    if not datos or 'nombre' not in datos or 'email' not in datos:
        return jsonify({'error': 'Datos incompletos'}), 400
    
    # Crear nuevo usuario
    nuevo_usuario = {
        'id': len(usuarios) + 1,
        'nombre': datos['nombre'],
        'email': datos['email']
    }
    
    usuarios.append(nuevo_usuario)
    
    return jsonify({
        'mensaje': 'Usuario creado exitosamente',
        'usuario': nuevo_usuario
    }), 201

PUT - Actualizar Datos

# Actualizar usuario completo
@app.route('/api/usuarios/<int:id>', methods=['PUT'])
def actualizar_usuario(id):
    usuario = next((u for u in usuarios if u['id'] == id), None)
    
    if not usuario:
        return jsonify({'error': 'Usuario no encontrado'}), 404
    
    datos = request.get_json()
    
    # Validar datos
    if not datos or 'nombre' not in datos or 'email' not in datos:
        return jsonify({'error': 'Datos incompletos'}), 400
    
    # Actualizar
    usuario['nombre'] = datos['nombre']
    usuario['email'] = datos['email']
    
    return jsonify({
        'mensaje': 'Usuario actualizado',
        'usuario': usuario
    }), 200

PATCH - Actualización Parcial

# Actualizar campos específicos
@app.route('/api/usuarios/<int:id>', methods=['PATCH'])
def actualizar_parcial_usuario(id):
    usuario = next((u for u in usuarios if u['id'] == id), None)
    
    if not usuario:
        return jsonify({'error': 'Usuario no encontrado'}), 404
    
    datos = request.get_json()
    
    # Actualizar solo campos proporcionados
    if 'nombre' in datos:
        usuario['nombre'] = datos['nombre']
    if 'email' in datos:
        usuario['email'] = datos['email']
    
    return jsonify({
        'mensaje': 'Usuario actualizado parcialmente',
        'usuario': usuario
    }), 200

DELETE - Eliminar Datos

# Eliminar usuario
@app.route('/api/usuarios/<int:id>', methods=['DELETE'])
def eliminar_usuario(id):
    global usuarios
    
    usuario = next((u for u in usuarios if u['id'] == id), None)
    
    if not usuario:
        return jsonify({'error': 'Usuario no encontrado'}), 404
    
    usuarios = [u for u in usuarios if u['id'] != id]
    
    return jsonify({'mensaje': 'Usuario eliminado exitosamente'}), 200

Manejo de Errores

# Error 404
@app.errorhandler(404)
def not_found(error):
    return jsonify({'error': 'Recurso no encontrado'}), 404

# Error 500
@app.errorhandler(500)
def internal_error(error):
    return jsonify({'error': 'Error interno del servidor'}), 500

# Error 400
@app.errorhandler(400)
def bad_request(error):
    return jsonify({'error': 'Solicitud incorrecta'}), 400

# Manejo personalizado de excepciones
from werkzeug.exceptions import BadRequest

@app.errorhandler(BadRequest)
def handle_bad_request(e):
    return jsonify({
        'error': 'Datos inválidos',
        'mensaje': str(e)
    }), 400

Validación de Datos

def validar_email(email):
    import re
    patron = r'^[\w\.-]+@[\w\.-]+\.\w+$'
    return re.match(patron, email) is not None

@app.route('/api/usuarios', methods=['POST'])
def crear_usuario_validado():
    datos = request.get_json()
    
    # Validaciones
    if not datos:
        return jsonify({'error': 'No se enviaron datos'}), 400
    
    if 'nombre' not in datos or not datos['nombre'].strip():
        return jsonify({'error': 'El nombre es requerido'}), 400
    
    if 'email' not in datos:
        return jsonify({'error': 'El email es requerido'}), 400
    
    if not validar_email(datos['email']):
        return jsonify({'error': 'Email inválido'}), 400
    
    # Verificar email duplicado
    if any(u['email'] == datos['email'] for u in usuarios):
        return jsonify({'error': 'El email ya existe'}), 409
    
    # Crear usuario
    nuevo_usuario = {
        'id': len(usuarios) + 1,
        'nombre': datos['nombre'],
        'email': datos['email']
    }
    
    usuarios.append(nuevo_usuario)
    
    return jsonify(nuevo_usuario), 201

CORS - Cross-Origin Resource Sharing

from flask_cors import CORS

app = Flask(__name__)

# Permitir CORS para todas las rutas
CORS(app)

# O configuración específica
CORS(app, resources={
    r"/api/*": {
        "origins": ["http://localhost:3000"],
        "methods": ["GET", "POST", "PUT", "DELETE"],
        "allow_headers": ["Content-Type"]
    }
})

Paginación

@app.route('/api/usuarios', methods=['GET'])
def obtener_usuarios_paginados():
    # Obtener parámetros de query
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    # Calcular índices
    start = (page - 1) * per_page
    end = start + per_page
    
    # Paginar
    usuarios_paginados = usuarios[start:end]
    
    return jsonify({
        'usuarios': usuarios_paginados,
        'page': page,
        'per_page': per_page,
        'total': len(usuarios),
        'pages': (len(usuarios) + per_page - 1) // per_page
    }), 200

Filtros y Búsqueda

@app.route('/api/usuarios/buscar', methods=['GET'])
def buscar_usuarios():
    # Obtener parámetro de búsqueda
    query = request.args.get('q', '').lower()
    
    if not query:
        return jsonify({'error': 'Parámetro de búsqueda requerido'}), 400
    
    # Filtrar usuarios
    resultados = [
        u for u in usuarios 
        if query in u['nombre'].lower() or query in u['email'].lower()
    ]
    
    return jsonify({
        'resultados': resultados,
        'total': len(resultados)
    }), 200

Integración con Base de Datos SQLite

from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///api.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# Modelo
class Usuario(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    nombre = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(100), unique=True, nullable=False)
    fecha_creacion = db.Column(db.DateTime, default=datetime.utcnow)
    
    def to_dict(self):
        return {
            'id': self.id,
            'nombre': self.nombre,
            'email': self.email,
            'fecha_creacion': self.fecha_creacion.isoformat()
        }

# Crear tablas
with app.app_context():
    db.create_all()

# CRUD con base de datos
@app.route('/api/usuarios', methods=['GET'])
def obtener_usuarios_db():
    usuarios = Usuario.query.all()
    return jsonify([u.to_dict() for u in usuarios]), 200

@app.route('/api/usuarios', methods=['POST'])
def crear_usuario_db():
    datos = request.get_json()
    
    try:
        nuevo_usuario = Usuario(
            nombre=datos['nombre'],
            email=datos['email']
        )
        
        db.session.add(nuevo_usuario)
        db.session.commit()
        
        return jsonify(nuevo_usuario.to_dict()), 201
    
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 400

@app.route('/api/usuarios/<int:id>', methods=['GET'])
def obtener_usuario_db(id):
    usuario = Usuario.query.get_or_404(id)
    return jsonify(usuario.to_dict()), 200

@app.route('/api/usuarios/<int:id>', methods=['PUT'])
def actualizar_usuario_db(id):
    usuario = Usuario.query.get_or_404(id)
    datos = request.get_json()
    
    usuario.nombre = datos.get('nombre', usuario.nombre)
    usuario.email = datos.get('email', usuario.email)
    
    try:
        db.session.commit()
        return jsonify(usuario.to_dict()), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 400

@app.route('/api/usuarios/<int:id>', methods=['DELETE'])
def eliminar_usuario_db(id):
    usuario = Usuario.query.get_or_404(id)
    
    try:
        db.session.delete(usuario)
        db.session.commit()
        return jsonify({'mensaje': 'Usuario eliminado'}), 200
    except Exception as e:
        db.session.rollback()
        return jsonify({'error': str(e)}), 400

Autenticación Básica con JWT

pip install flask-jwt-extended
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity

app.config['JWT_SECRET_KEY'] = 'tu-clave-secreta-super-segura'
jwt = JWTManager(app)

# Login
@app.route('/api/login', methods=['POST'])
def login():
    datos = request.get_json()
    
    email = datos.get('email')
    password = datos.get('password')
    
    # Validar credenciales (ejemplo simple)
    if email == 'admin@email.com' and password == 'password123':
        access_token = create_access_token(identity=email)
        return jsonify({
            'access_token': access_token,
            'token_type': 'Bearer'
        }), 200
    
    return jsonify({'error': 'Credenciales inválidas'}), 401

# Ruta protegida
@app.route('/api/protegida', methods=['GET'])
@jwt_required()
def ruta_protegida():
    usuario_actual = get_jwt_identity()
    return jsonify({
        'mensaje': 'Acceso permitido',
        'usuario': usuario_actual
    }), 200

Testing de la API

# test_api.py
import unittest
import json
from app import app, db, Usuario

class APITestCase(unittest.TestCase):
    def setUp(self):
        app.config['TESTING'] = True
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
        self.app = app.test_client()
        
        with app.app_context():
            db.create_all()
    
    def tearDown(self):
        with app.app_context():
            db.session.remove()
            db.drop_all()
    
    def test_obtener_usuarios(self):
        response = self.app.get('/api/usuarios')
        self.assertEqual(response.status_code, 200)
    
    def test_crear_usuario(self):
        datos = {
            'nombre': 'Test Usuario',
            'email': 'test@email.com'
        }
        response = self.app.post(
            '/api/usuarios',
            data=json.dumps(datos),
            content_type='application/json'
        )
        self.assertEqual(response.status_code, 201)
        self.assertIn('id', json.loads(response.data))

if __name__ == '__main__':
    unittest.main()

Documentación con Swagger

pip install flask-swagger-ui
from flask_swagger_ui import get_swaggerui_blueprint

SWAGGER_URL = '/api/docs'
API_URL = '/static/swagger.json'

swagger_blueprint = get_swaggerui_blueprint(
    SWAGGER_URL,
    API_URL,
    config={'app_name': "Mi API"}
)

app.register_blueprint(swagger_blueprint, url_prefix=SWAGGER_URL)

Ejemplo Completo: API de Tareas

# app.py - API completa
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from datetime import datetime

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tareas.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
CORS(app)

class Tarea(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    titulo = db.Column(db.String(200), nullable=False)
    descripcion = db.Column(db.Text)
    completada = db.Column(db.Boolean, default=False)
    fecha_creacion = db.Column(db.DateTime, default=datetime.utcnow)
    fecha_vencimiento = db.Column(db.DateTime)
    
    def to_dict(self):
        return {
            'id': self.id,
            'titulo': self.titulo,
            'descripcion': self.descripcion,
            'completada': self.completada,
            'fecha_creacion': self.fecha_creacion.isoformat(),
            'fecha_vencimiento': self.fecha_vencimiento.isoformat() if self.fecha_vencimiento else None
        }

with app.app_context():
    db.create_all()

# Endpoints
@app.route('/api/tareas', methods=['GET'])
def obtener_tareas():
    tareas = Tarea.query.all()
    return jsonify([t.to_dict() for t in tareas]), 200

@app.route('/api/tareas', methods=['POST'])
def crear_tarea():
    datos = request.get_json()
    
    nueva_tarea = Tarea(
        titulo=datos['titulo'],
        descripcion=datos.get('descripcion', ''),
        fecha_vencimiento=datetime.fromisoformat(datos['fecha_vencimiento']) if 'fecha_vencimiento' in datos else None
    )
    
    db.session.add(nueva_tarea)
    db.session.commit()
    
    return jsonify(nueva_tarea.to_dict()), 201

@app.route('/api/tareas/<int:id>/completar', methods=['PATCH'])
def completar_tarea(id):
    tarea = Tarea.query.get_or_404(id)
    tarea.completada = not tarea.completada
    db.session.commit()
    return jsonify(tarea.to_dict()), 200

if __name__ == '__main__':
    app.run(debug=True)

Probar la API con curl

# GET - Obtener todos
curl http://localhost:5000/api/usuarios

# GET - Obtener uno
curl http://localhost:5000/api/usuarios/1

# POST - Crear
curl -X POST http://localhost:5000/api/usuarios \
  -H "Content-Type: application/json" \
  -d '{"nombre":"Ana","email":"ana@email.com"}'

# PUT - Actualizar
curl -X PUT http://localhost:5000/api/usuarios/1 \
  -H "Content-Type: application/json" \
  -d '{"nombre":"Ana García","email":"ana@email.com"}'

# DELETE - Eliminar
curl -X DELETE http://localhost:5000/api/usuarios/1

Buenas Prácticas

  • Versionado: Usa /api/v1/ en tus rutas
  • Códigos HTTP correctos: 200, 201, 400, 404, 500
  • Validación: Siempre valida entrada del usuario
  • Documentación: Documenta tus endpoints
  • Seguridad: Usa HTTPS, valida tokens, sanitiza input
  • Rate limiting: Limita requests por IP
  • Logs: Registra todas las operaciones

Conclusión

Crear APIs REST con Flask es simple y poderoso. Esta base te permite construir servicios web escalables que pueden consumirse desde cualquier aplicación frontend, móvil o backend. Practica creando diferentes endpoints y agregando funcionalidades más complejas.