Si quieres empezar en el mundo del backend con Python, tarde o temprano vas a encontrarte con las APIs REST. En este tutorial vas a aprender qué son, cómo funcionan y, sobre todo, cómo crear una desde cero.
Para ello vamos a utilizar Flask, un framework de Python ligero y sencillo que nos permite crear servidores web y APIs sin añadir una complejidad innecesaria.
La idea es ir directamente a la práctica: en unos 20 minutos tendrás tu propio servidor funcionando en tu ordenador, con diferentes rutas, conexión a una base de datos y endpoints que reciben y devuelven datos en formato JSON.
No necesitas experiencia previa con Flask. Vamos a construir la API paso a paso para que entiendas qué estamos haciendo y por qué.
¿Qué es Python backend?
Seguro que has usado webs donde haces algo (registrarte, comprar, dar a "me gusta") y esa acción queda guardada. La ves después desde otro dispositivo. Esa parte invisible que procesa y guarda datos es el backend.
Python puede hacer eso. El frontend (el botón, la animación) es lo que ve el usuario. El backend Python recibe la petición, la procesa, guarda el dato en la base de datos y devuelve una respuesta. Con Flask, una librería pequeña, levantas ese servidor en cuatro líneas.
¿Qué es una API REST?
El frontend nunca toca la base de datos directamente. No sería seguro ni práctico: cualquiera podría meter la mano desde el navegador. Necesita un intermediario que reciba la petición, la valide y hable con la base de datos por su cuenta. Ese intermediario es la API.
Piensa en un restaurante: tú no entras en la cocina a servirte, le pides al camarero lo que quieres de la carta. El camarero lleva la nota a la cocina y te trae el plato. La API hace justo eso entre el frontend y el backend: cuando das a "me gusta" en una app, el botón no escribe en la base de datos, le pide a la API que lo haga, y la API le devuelve una confirmación.
REST es simplemente la forma más habitual de organizar esa carta. En vez de inventarte una URL distinta para cada acción, agrupas todo alrededor de recursos: /api/tareas es la lista de tareas, /api/tareas/3 es la tarea con id 3. Y para decir qué quieres hacer con ese recurso usas el método HTTP, que funciona como el verbo de la frase: GET para leer, POST para crear, PUT para modificar, DELETE para borrar. Misma URL, verbo distinto, acción distinta.
Aquí es donde mucha gente se lía al principio: prueba a entrar en /api/tareas desde el navegador esperando crear algo, y lo único que consigue es un GET, porque el navegador solo sabe pedir así cuando escribes una URL directamente. Para hacer un POST hace falta otra herramienta, como verás más adelante con Postman.
Los datos viajan en JSON, texto con una estructura fija de claves y valores, fácil de leer tanto para una persona como para un programa. Si le mandas esto a /api/tareas con un POST:
{
"titulo": "Aprender Flask",
"completada": false
}
La API te responde con el recurso ya creado y un código de estado, un número que resume cómo ha ido la petición sin que tengas que leer ningún texto:
{
"id": 1,
"titulo": "Aprender Flask",
"completada": false
}
Los códigos que más vas a ver son 200 (todo bien), 201 (se ha creado el recurso), 400 (la petición está mal formada o falta un dato), 404 (el recurso no existe, por ejemplo /api/tareas/999) y 500 (algo ha petado en tu código Python). Cuando construyas la API de tareas más abajo vas a ver estos códigos aparecer de verdad, así que no hace falta memorizarlos ahora.
Antes de empezar
Necesitas conocimientos básicos de Python: variables, funciones y algo de sintaxis. Si sabes escribir un bucle for es suficiente. Si esto no te suena de nada, tómate antes 15 minutos con este tutorial de Python para principiantes. Está pensado justo para llegar preparado hasta aquí.
Este tutorial usa PyCharm de principio a fin. Desde la versión 2025.1 es un único producto (ya no hay edición Community separada): las funciones básicas son gratuitas y la suscripción Pro desbloquea lo avanzado. Descárgalo en jetbrains.com/pycharm antes de continuar. Las nuevas instalaciones incluyen 30 días de prueba Pro.
Paso 1: verificar que Python está instalado
Abre la terminal (en Windows: PowerShell o Símbolo del sistema; en Mac/Linux: Terminal) y escribe:
python --version
Si ves "Python 3.x.x", perfecto. Si no, ve a python.org y descarga la versión más reciente. Busca la 3.14.x, que es la versión estable actual.
Error frecuente: en Mac y Linux el comando puede ser python3 en lugar de python. Si el primero da "command not found", prueba python3 --version. Usa el que funcione de forma consistente durante todo el tutorial.
Paso 2: crea el proyecto y el entorno virtual
Un entorno virtual aisla los paquetes de cada proyecto. Sin él, Flask se instala de forma global y vas acumulando versiones de todos tus proyectos mezcladas. Con el tiempo eso genera conflictos difíciles de diagnosticar.
PyCharm crea el entorno virtual automáticamente al crear un nuevo proyecto:
- Abre PyCharm → New Project
- Elige la ubicación de la carpeta
- En "Python Interpreter" selecciona New Virtualenv Environment: PyCharm crea la carpeta
.venvsolo - Haz clic en Create
Para instalar paquetes tienes dos formas sin tocar la terminal. La más cómoda cuando copias código: PyCharm subraya en rojo cualquier import que no esté instalado. Pulsa Alt+Enter sobre la línea y aparece "Install package X". Un clic y listo, se instala en el entorno virtual del proyecto. Si ya sabes qué paquete quieres, también puedes buscarlo en View → Tool Windows → Python Packages.
Paso 3: instalar Flask
No hay que instalar nada todavía. En el paso siguiente, cuando pegues from flask import Flask en el archivo, PyCharm subraya la línea en rojo. Pulsa Alt+Enter → "Install package flask" y lo instala en el entorno virtual del proyecto automáticamente.
Paso 4: tu primer servidor web
En PyCharm ya tienes el proyecto abierto del paso anterior. Crea un archivo nuevo llamado app.py en la raíz del proyecto:
# app.py - Tu primer servidor Python
from flask import Flask
# Crear la aplicación Flask
app = Flask(__name__)
# Ruta principal - cuando alguien visite tu servidor
@app.route('/')
def inicio():
return "¡Hola mundo! Mi primer servidor Python funciona 🐍"
# Ejecutar el servidor
if __name__ == '__main__':
app.run(debug=True)
Para arrancarlo usa el botón Play de PyCharm, el triángulo verde arriba a la derecha. Usa el intérprete del proyecto y la carpeta correcta de forma automática. Si prefieres la terminal, usa la integrada de PyCharm (View → Tool Windows → Terminal): ya abre en la carpeta del proyecto con el entorno activo, escribe python app.py y listo.
La documentación oficial de Flask recomienda hoy arrancar con la CLI en lugar de python app.py: flask --app app run --debug. Hace lo mismo que app.run(debug=True), recarga automática incluida, pero es la forma que Flask documenta como estándar. Para este tutorial nos quedamos con el botón Play porque es más cómodo mientras aprendes, pero si ya te sientes cómodo en la terminal, usa la CLI.
Abre el navegador y ve a http://127.0.0.1:5000 o haz click manteniendo pulsada la tecla Ctrl. Si no sabes qué significa esa dirección, aquí te lo explico.
Si ves el mensaje, el servidor está funcionando. Para detenerlo pulsa Ctrl+C si lo has iniciado desde la consola o pulsa el boton cuadrado rojo de PyCharm si lo has iniciado desde el botón Play.
Paso 5: añadir más rutas
Un servidor real tiene múltiples endpoints. Cada ruta es una función de Python con un decorador @app.route que le dice a Flask qué URL la activa. Reemplaza el contenido de app.py con esto:
from flask import Flask, jsonify
app = Flask(__name__)
# Página principal
@app.route('/')
def inicio():
return "¡Bienvenido a mi servidor Python! 🐍"
# Página "acerca de"
@app.route('/sobre-mi')
def sobre_mi():
return "Soy un desarrollador Python principiante 💻"
# API que devuelve datos JSON
@app.route('/api/datos')
def obtener_datos():
datos = {
"mensaje": "¡Mi primera API funciona!",
"usuario": "Estudiante Python",
"nivel": "Principiante"
}
return jsonify(datos)
if __name__ == '__main__':
app.run(debug=True)
Reinicia el servidor (Ctrl+C y vuelve a ejecutar python app.py) y prueba estas URLs en el navegador:
http://127.0.0.1:5000/http://127.0.0.1:5000/sobre-mihttp://127.0.0.1:5000/api/datos(esta devuelve JSON)
Paso 6: conectar una base de datos
Un servidor sin base de datos no guarda nada. Vamos a usar PostgreSQL, que es lo que encontrarás en la mayoría de proyectos reales.
La forma más limpia de tenerlo en local es con Docker: un comando y tienes PostgreSQL corriendo sin instalar nada en el PC. Si aún no tienes Docker, aquí tienes el tutorial.
docker run --name prueba-postgres -e POSTGRES_PASSWORD=tu_password_seguro -e POSTGRES_DB=mi_app_flask -p 5432:5432 -v prueba-postgres-data:/var/lib/postgresql -d postgres:18
El :18 fija la versión estable actual de PostgreSQL (agosto 2026). Cuando salga la 19 o la 20, el comando seguirá funcionando igual porque apunta a una versión concreta, no a :latest, que es lo que pasa si escribes solo postgres y puede cambiar de un día para otro.
-e POSTGRES_DB=mi_app_flask es la parte que se olvida fácil: sin ella, el contenedor arranca con una única base de datos por defecto llamada postgres, no mi_app_flask. Esa variable es la que le dice a la imagen oficial que cree mi_app_flask la primera vez que arranca. Si te la saltas, más adelante db.create_all() falla con FATAL: database "mi_app_flask" does not exist porque la base de datos a la que apunta tu .env nunca llegó a existir.
-v prueba-postgres-data:/var/lib/postgresql es el volumen: sin él, los datos viven solo dentro del contenedor, y si lo borras con docker rm desaparecen para siempre. Con el volumen, los datos quedan en Docker aunque elimines y vuelvas a crear el contenedor.
Cuando no lo necesites, docker stop prueba-postgres lo pausa sin borrar nada. Los datos siguen ahí para cuando lo vuelvas a arrancar con docker start prueba-postgres. Con el volumen ya puesto, ni siquiera docker rm prueba-postgres te hace perder los datos: seguirían en el volumen para el siguiente contenedor que lo use.
❌ Error común: "address already in use" o "port is already allocated"
Si ya tienes PostgreSQL instalado directamente en tu ordenador (sin Docker), el puerto 5432 va a estar ocupado y el docker run va a fallar. Cambia el primer número del -p por otro libre, por ejemplo -p 5433:5432, y ajusta el puerto en tu DATABASE_URL más abajo para que coincida.
Las credenciales de la base de datos no van escritas en el código. Es una regla desde el primer proyecto: si subes el código a GitHub con la contraseña dentro, queda expuesta para siempre en el historial de git. Crea un fichero .env en la raíz del proyecto con este contenido:
DATABASE_URL=postgresql://postgres:tu_password_seguro@localhost:5432/mi_app_flask
Y añade .env a tu .gitignore para que git no lo rastree nunca. Ahora el código lee la URL de la BD desde esa variable:
import os
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
load_dotenv()
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
db = SQLAlchemy(app)
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(120), unique=True, nullable=False)
@app.route('/api/usuarios')
def obtener_usuarios():
usuarios = Usuario.query.all()
return jsonify([{'id': u.id, 'nombre': u.nombre, 'email': u.email} for u in usuarios])
@app.route('/api/usuarios', methods=['POST'])
def crear_usuario():
datos = request.get_json()
# TODO: validar que 'nombre' y 'email' llegan en el JSON antes de usarlos
# (si faltan, esto revienta con un 500 en vez de un 400 controlado)
nuevo = Usuario(nombre=datos['nombre'], email=datos['email'])
db.session.add(nuevo)
db.session.commit()
return jsonify({'mensaje': 'Usuario creado correctamente'})
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Cuando PyCharm vea from dotenv import load_dotenv y from flask_sqlalchemy import SQLAlchemy subrayados en rojo, pulsa Alt+Enter en cada uno e instala los paquetes python-dotenv y flask-sqlalchemy. Lo mismo con psycopg2-binary si PyCharm lo pide al conectar.
db.create_all() dentro del app_context() crea la tabla usuario en PostgreSQL si no existe, sin escribir SQL a mano. Usuario.query.all() devuelve objetos Python, no tuplas. El ORM gestiona el pool de conexiones internamente; no hay que cerrar nada.
El error más frecuente aquí: sqlalchemy.exc.OperationalError: could not connect to server. Significa que PostgreSQL no está arrancado. Comprueba que el contenedor está corriendo con docker ps antes de lanzar Flask.
Paso 7: probar tu API
Reinicia el servidor y abre en el navegador:
http://127.0.0.1:5000/api/usuarios
Para crear un usuario necesitas enviar un POST con JSON, y eso el navegador no lo hace solo. La forma más fiable es Postman (gratuito, funciona en Windows sin configurar nada). Selecciona método POST, pega la URL http://127.0.0.1:5000/api/usuarios, ve a Body → raw → JSON y escribe:
{"nombre": "Juan Pérez", "email": "juan@email.com"}
Le das a Send y ves la respuesta. Después refresca el GET en el navegador y ya aparece en la lista.
Proyecto completo: sistema de tareas
Con lo que ya sabes montas una API CRUD completa. Vamos a usar una base de datos nueva, tareas_db, para no mezclarla con mi_app_flask. No vuelvas a ejecutar docker run: el contenedor prueba-postgres ya está corriendo, y si repites el mismo comando Docker te dará Error: Conflict. The container name "/prueba-postgres" is already in use. Lo que hace falta es crear la base de datos nueva dentro de ese mismo contenedor:
docker exec -it prueba-postgres createdb -U postgres tareas_db
Ahora sí, actualiza tu .env apuntando a esa base de datos:
DATABASE_URL=postgresql://postgres:tu_password_seguro@localhost:5432/tareas_db
Aquí tienes la API completa con GET, POST, PUT y DELETE:
import os
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime, timezone
load_dotenv()
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
db = SQLAlchemy(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=lambda: datetime.now(timezone.utc))
@app.route('/api/tareas')
def obtener_tareas():
tareas = Tarea.query.order_by(Tarea.fecha_creacion.desc()).all()
return jsonify([{
'id': t.id,
'titulo': t.titulo,
'descripcion': t.descripcion,
'completada': t.completada,
'fecha_creacion': t.fecha_creacion.isoformat()
} for t in tareas])
@app.route('/api/tareas', methods=['POST'])
def crear_tarea():
datos = request.get_json()
# TODO: validar que 'titulo' llega en el JSON antes de usarlo
nueva = Tarea(titulo=datos['titulo'], descripcion=datos.get('descripcion', ''))
db.session.add(nueva)
db.session.commit()
return jsonify({'mensaje': 'Tarea creada correctamente'})
@app.route('/api/tareas/<int:tarea_id>', methods=['PUT'])
def completar_tarea(tarea_id):
tarea = db.get_or_404(Tarea, tarea_id)
tarea.completada = True
db.session.commit()
return jsonify({'mensaje': 'Tarea completada'})
@app.route('/api/tareas/<int:tarea_id>', methods=['DELETE'])
def borrar_tarea(tarea_id):
tarea = db.get_or_404(Tarea, tarea_id)
db.session.delete(tarea)
db.session.commit()
return jsonify({'mensaje': 'Tarea eliminada'})
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
db.get_or_404() es el mismo que usa completar_tarea: si el id no existe, Flask devuelve un 404 solo, sin que tengas que comprobarlo tú a mano. db.session.delete() marca el objeto para borrar y commit() lo hace efectivo en PostgreSQL, igual que con los otros endpoints.
Si algún día quieres publicar esto en internet
debug=True es solo para desarrollo. En un servidor real expone el depurador de Flask al exterior, lo que es un agujero de seguridad grave. Cuando subas tu app a producción, cámbialo a debug=False o usa variables de entorno.
Con Flask-SQLAlchemy no hay que cerrar conexiones manualmente: el ORM gestiona un pool de conexiones internamente. Donde sí tienes que tener cuidado es si mezclas consultas ORM con SQL crudo: usa siempre parámetros bindados, nunca strings concatenados directamente.
Qué toca ahora
El siguiente paso concreto es añadir manejo de errores a la API: qué pasa si alguien hace un POST sin enviar el campo nombre, o si el id que pasa en la URL no existe. Eso es lo que convierte un ejercicio en una API que aguanta uso real. Lo resolvemos en Tu API de Flask ha crecido: así se reorganiza sin reescribirla, que retoma justo el TODO de validación que dejamos aquí sin resolver.
Si quieres entender qué está haciendo el ORM por debajo (qué SQL genera cada consulta), repasa SQL básico: INSERT, SELECT, UPDATE y DELETE. No es imprescindible, pero ayuda mucho cuando algo falla.
Cuando el manejo de errores funcione, conecta esta API con una página HTML usando JavaScript Fetch. En ese momento el backend deja de ser un ejercicio y se convierte en una aplicación completa.