Proyecto: API REST en Python con Flask, SQLAlchemy y JWT (ToDo)

python Proyecto: API REST en Python con Flask, SQLAlchemy y JWT (ToDo)

Proyecto práctico: API REST con Flask, SQLAlchemy y JWT (ToDo)

Construiremos una API minimalista para gestionar tareas (ToDo) con registro/login, tokens JWT y acceso por propietario. El objetivo es un proyecto real, fácil de extender y seguro por defecto.

Requisitos previos

  • Python 3.8+
  • pip
  • Conocimientos básicos de HTTP, REST y Python

Estructura del proyecto

todo-api/
├─ app.py
├─ run.py
├─ config.py
├─ extensions.py
├─ models.py
├─ auth.py
├─ tasks.py
├─ requirements.txt
└─ data.db  (creado al ejecutar)

requirements.txt

flask
flask-sqlalchemy
flask-jwt-extended
passlib[bcrypt]

1) config.py

import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///data.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'jwt-secret-for-dev')

2) extensions.py

from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager

db = SQLAlchemy()
jwt = JWTManager()

3) models.py

from extensions import db
from datetime import datetime
from passlib.hash import bcrypt

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    tasks = db.relationship('Task', backref='owner', lazy=True)

    def set_password(self, password):
        self.password_hash = bcrypt.hash(password)

    def check_password(self, password):
        return bcrypt.verify(password, self.password_hash)

class Task(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(140), nullable=False)
    description = db.Column(db.Text)
    done = db.Column(db.Boolean, default=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

4) auth.py (registro y login)

from flask import Blueprint, request, jsonify
from extensions import db
from models import User
from flask_jwt_extended import create_access_token

auth_bp = Blueprint('auth', __name__, url_prefix='/auth')

@auth_bp.route('/register', methods=['POST'])
def register():
    data = request.get_json() or {}
    username = data.get('username')
    password = data.get('password')
    if not username or not password:
        return jsonify({'msg': 'username and password required'}), 400
    if User.query.filter_by(username=username).first():
        return jsonify({'msg': 'user exists'}), 400
    user = User(username=username)
    user.set_password(password)
    db.session.add(user)
    db.session.commit()
    return jsonify({'id': user.id, 'username': user.username}), 201

@auth_bp.route('/login', methods=['POST'])
def login():
    data = request.get_json() or {}
    username = data.get('username')
    password = data.get('password')
    user = User.query.filter_by(username=username).first()
    if not user or not user.check_password(password):
        return jsonify({'msg': 'Bad credentials'}), 401
    access = create_access_token(identity=user.id)
    return jsonify({'access_token': access})

5) tasks.py (endpoints CRUD)

from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from extensions import db
from models import Task

tasks_bp = Blueprint('tasks', __name__, url_prefix='/tasks')

@tasks_bp.route('', methods=['GET'])
@jwt_required()
def list_tasks():
    user_id = get_jwt_identity()
    tasks = Task.query.filter_by(user_id=user_id).all()
    return jsonify([
        {'id': t.id, 'title': t.title, 'description': t.description, 'done': t.done, 'created_at': t.created_at.isoformat()}
        for t in tasks
    ])

@tasks_bp.route('', methods=['POST'])
@jwt_required()
def create_task():
    user_id = get_jwt_identity()
    data = request.get_json() or {}
    title = data.get('title')
    if not title:
        return jsonify({'msg': 'title required'}), 400
    task = Task(title=title, description=data.get('description', ''), user_id=user_id)
    db.session.add(task)
    db.session.commit()
    return jsonify({'id': task.id, 'title': task.title}), 201

@tasks_bp.route('/', methods=['GET'])
@jwt_required()
def get_task(task_id):
    user_id = get_jwt_identity()
    task = Task.query.get_or_404(task_id)
    if task.user_id != user_id:
        return jsonify({'msg': 'Forbidden'}), 403
    return jsonify({'id': task.id, 'title': task.title, 'description': task.description, 'done': task.done, 'created_at': task.created_at.isoformat()})

@tasks_bp.route('/', methods=['PUT'])
@jwt_required()
def update_task(task_id):
    user_id = get_jwt_identity()
    task = Task.query.get_or_404(task_id)
    if task.user_id != user_id:
        return jsonify({'msg': 'Forbidden'}), 403
    data = request.get_json() or {}
    task.title = data.get('title', task.title)
    task.description = data.get('description', task.description)
    if 'done' in data:
        task.done = bool(data.get('done'))
    db.session.commit()
    return jsonify({'msg': 'updated'})

@tasks_bp.route('/', methods=['DELETE'])
@jwt_required()
def delete_task(task_id):
    user_id = get_jwt_identity()
    task = Task.query.get_or_404(task_id)
    if task.user_id != user_id:
        return jsonify({'msg': 'Forbidden'}), 403
    db.session.delete(task)
    db.session.commit()
    return jsonify({'msg': 'deleted'})

6) app.py (factory)

from flask import Flask
from config import Config
from extensions import db, jwt
from auth import auth_bp
from tasks import tasks_bp

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    jwt.init_app(app)

    app.register_blueprint(auth_bp)
    app.register_blueprint(tasks_bp)

    with app.app_context():
        # Para desarrollo rápido: crea tablas si no existen
        db.create_all()

    return app

7) run.py

from app import create_app

app = create_app()

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

Instalación y ejecución

python -m venv venv
source venv/bin/activate  # o venv\Scripts\activate en Windows
pip install -r requirements.txt
python run.py

Probar la API (ejemplos curl)

# Registrar
curl -X POST http://127.0.0.1:5000/auth/register -H 'Content-Type: application/json' -d '{"username":"alice","password":"secret"}'

# Login -> obtén token
curl -X POST http://127.0.0.1:5000/auth/login -H 'Content-Type: application/json' -d '{"username":"alice","password":"secret"}'

# Imagina que obtienes {"access_token":"..."}
TOKEN=eyJ... 

# Crear tarea
curl -X POST http://127.0.0.1:5000/tasks -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" -d '{"title":"Comprar leche","description":"2 litros"}'

# Listar tareas
curl -X GET http://127.0.0.1:5000/tasks -H "Authorization: Bearer $TOKEN"

Por qué estas decisiones (no solo cómo)

  • Flask + SQLAlchemy: ligera, fácil de entender y con suficiente potencia para APIs medianas. SQLAlchemy te da portabilidad entre motores y un ORM limpio.
  • JWT (flask-jwt-extended): permite una API stateless, adecuada para SPAs o móviles. Aquí usamos el ID de usuario como identidad para verificar recursos.
  • passlib + bcrypt: nunca guardes contraseñas en texto. passlib ofrece hashing seguro y actualizable.
  • Blueprints y create_app: separación de responsabilidades y testabilidad. Con create_app puedes crear instancias en tests con configuraciones distintas.
  • db.create_all() en dev: rápido para arrancar. En producción usa migraciones (Flask-Migrate / Alembic) para cambios estructurales controlados.

Puntos importantes de seguridad y buenas prácticas

  • No expongas SECRET_KEY ni JWT_SECRET_KEY en el código. Usa variables de entorno o servicios de secretos.
  • Los tokens JWT deben tener expiración configurable; considera usar refresh tokens y revocación (lista negra) si necesitas invalidar sesión.
  • Valida y sanea la entrada. En el ejemplo hicimos validaciones mínimas; para producción añade esquemas (pydantic, marshmallow) y límites de tamaño.
  • Considera CORS, rate limiting y logging correcto antes de desplegar.

Siguientes pasos prácticos

  • Agregar Flask-Migrate para control de esquemas y migraciones.
  • Implementar refresh tokens y logout (revocación de tokens).
  • Agregar tests unitarios e integración (pytest + pytest-flask).
  • Deploy: configurar Gunicorn + Nginx, usar PostgreSQL en producción y habilitar HTTPS.

Consejo avanzado: para APIs con muchos clientes considera un sistema de revocación de JWT (p. ej. blacklist con Redis) y rotación periódica de la clave JWT_SECRET_KEY; además, audita los scopes/claims en el token para permisos más finos.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión