Module 2: Path Operations
DELETE: Eliminar Recursos
Descripción de la cápsula
Ya dominas leer (GET), crear (POST) y actualizar (PUT, PATCH). Falta la última operación del CRUD: eliminar. DELETE es el verbo HTTP que dice "quiero borrar este recurso." En esta cápsula implementarás un endpoint DELETE que elimina un libro por su ID, retorna una confirmación apropiada, y maneja el caso cuando el recurso no existe.
A diferencia de POST, PUT y PATCH, DELETE no recibe body — solo necesita el ID en la URL. La operación es la más simple del CRUD en términos de entrada: el cliente indica qué recurso eliminar mediante el path, y el servidor lo borra o responde que no existe. Pero sí hay decisiones de diseño que afectan la experiencia del consumidor de tu API: ¿retornas el recurso eliminado o solo una confirmación? ¿Usas status code 200 (con body) o 204 (sin body)? Estas elecciones influyen en cómo los clientes integran tu API.
Al terminar tendrás el CRUD completo: las cinco operaciones funcionando sobre tu colección de libros en memoria. Con GET, POST, PUT, PATCH y DELETE cubrirás el ciclo de vida completo de un recurso en una API REST.
DELETE: El concepto
DELETE elimina un recurso por su ID. Es la operación más simple conceptualmente: busca el recurso, si existe lo elimina, si no existe retorna un error.
Idempotencia: DELETE es idempotente. Ejecutarlo una vez elimina el recurso. Ejecutarlo de nuevo ya no encuentra nada — pero el estado final (recurso ausente) es el mismo. No es como POST, donde cada ejecución crea algo nuevo.
Idempotencia en detalle
¿Por qué se considera DELETE idempotente? La idea es que ejecutar la misma operación varias veces produce el mismo estado final. La primera vez que llamas DELETE /books/3, el libro desaparece. Si llamas DELETE /books/3 otra vez, el recurso ya no existe — pero el resultado es el mismo: no hay libro con ID 3. El cliente puede repetir el request (por ejemplo, tras un timeout o una red inestable) sin crear efectos secundarios distintos.
En contraste, POST no es idempotente: cada POST /books crea un recurso nuevo. Si un cliente reenvía el mismo POST por un error de red, terminarías con duplicados. Con DELETE, el cliente puede reintentar con tranquilidad: el estado final (recurso eliminado) es idéntico.
Esto influye en el diseño de APIs: los clientes HTTP suelen reintentar requests idempotentes automáticamente. DELETE y GET comparten esa propiedad; POST no.
Soft delete vs Hard delete
En este módulo implementas hard delete: el recurso se borra de la lista y desaparece para siempre. En aplicaciones reales a menudo se usa soft delete: en lugar de eliminar el registro, se marca como eliminado (por ejemplo, con un campo deleted_at o is_deleted). Los listados filtran estos registros, pero los datos siguen en la base para auditoría, recuperación o cumplimiento legal.
| Enfoque | Qué hace | Ventajas | Desventajas |
|---|---|---|---|
| Hard delete | Borra el recurso físicamente | Simple, no ocupa espacio | Sin recuperación, sin historial |
| Soft delete | Marca como eliminado (deleted_at, is_deleted) | Recuperable, auditoría, cumplimiento | Requiere filtros en queries, más lógica |
Para este módulo, hard delete es suficiente. Conocer la diferencia te prepara para APIs más complejas donde soft delete es común (usuarios, pedidos, facturas).
DELETE en contexto: relaciones y cascadas
En una API real con base de datos, eliminar un recurso puede afectar a otros. Por ejemplo, si borras un libro que tiene reseñas asociadas, ¿qué pasa con esas reseñas? Las opciones típicas son: cascada (eliminar también las reseñas), restringir (prohibir el DELETE si hay reseñas) o poner en null la referencia. Con una lista en memoria no tienes relaciones; cada libro es independiente. Más adelante, cuando uses SQLAlchemy u otro ORM, configurarás el comportamiento de eliminación en cascada en los modelos.
Seguridad y confirmación
DELETE es una operación destructiva. En producción, muchas APIs requieren confirmación explícita (por ejemplo, un query param confirm=true o un body con la contraseña del usuario). Algunos sistemas implementan "papelera" o soft delete para que el usuario pueda recuperar accidentalmente. Para este módulo, un simple books.remove(existing) es suficiente; en el Módulo 5 añadirás manejo de errores más robusto y podrás considerar capas de seguridad adicionales.
Probar DELETE con curl
Para probar desde la terminal:
# Eliminar el libro con ID 3
curl -X DELETE http://127.0.0.1:8000/books/3
# Verificar que ya no existe
curl -s http://127.0.0.1:8000/books/3
# Esperado: {"error": "Book not found"}
Con -v (verbose) verás los headers de respuesta, incluido el status code. Si tu endpoint retorna 200 con body, verás algo como {"message": "Book deleted", "id": 3}. Si configuras 204, la respuesta no tendrá body.
Integración DELETE en el flujo CRUD
DELETE cierra el ciclo de vida del recurso. Tras crear (POST), leer (GET) y actualizar (PUT/PATCH), el cliente puede eliminar el recurso con DELETE. Una secuencia típica en una interfaz: el usuario confirma la eliminación → el frontend llama DELETE → si recibe 200/204, redirige a la lista o muestra un mensaje de éxito; si recibe 404, muestra "recurso no encontrado". Como DELETE es idempotente, reintentar tras un timeout no genera efectos secundarios adversos: el recurso ya no existe y el estado final es el mismo.
Tu primer endpoint DELETE
Código básico
Agrega este endpoint a tu app/main.py (con la lista books, find_book, y los endpoints GET, POST, PUT, PATCH):
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return {"message": "Book deleted", "id": book_id}
Cómo funciona
@app.delete("/books/{book_id}")— Registra la ruta para el verbo DELETEbook_id: int— Extrae el ID de la URL (no hay body en DELETE)find_book(book_id)— Busca el libro- Si no existe, retorna error
books.remove(existing)— Remueve el libro de la lista- Retorna confirmación con el ID eliminado
Alternativa con pop() e índice
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
for index, book in enumerate(books):
if book["id"] == book_id:
deleted = books.pop(index)
return {"message": "Book deleted", "id": book_id, "deleted": deleted}
return {"error": "Book not found"}
pop(index) remueve y retorna el elemento. Útil si quieres incluir el recurso eliminado en la respuesta.
Status codes: 200 vs 204
Status 200 OK (con body)
Tu implementación actual retorna 200 con un body de confirmación:
{"message": "Book deleted", "id": 3}
Ventajas: El cliente recibe confirmación explícita. Puedes incluir el recurso eliminado si lo necesitas.
Status 204 No Content (sin body)
Según la especificación HTTP, 204 es el código "correcto" para DELETE exitoso cuando no hay contenido que retornar. Pero 204 significa que la respuesta no tiene body — no puedes enviar {"message": "deleted"} ni {"error": "not found"} con un 204.
Para usar 204 correctamente necesitarías:
- Retornar
Response(status_code=204)sin body en éxito - Usar
HTTPException(status_code=404)para "not found" (Módulo 5)
Recomendación para este módulo
Usa 200 con body por simplicidad. Te permite retornar tanto confirmación como mensajes de error de forma consistente. En el Módulo 5, cuando implementes HTTPException para errores, podrás considerar 204 para el caso exitoso.
Si quieres usar 204
from fastapi import Response
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"} # Sigue retornando 200 con body para errores
books.remove(existing)
return Response(status_code=204) # Sin body
Verificar la eliminación
Después de DELETE, el recurso debe desaparecer de la lista. Verifica con GET:
# 1. Eliminar libro con id 3
curl -X DELETE http://127.0.0.1:8000/books/3
# 2. Verificar que ya no existe
curl http://127.0.0.1:8000/books/3
# → {"error": "Book not found"}
# 3. Verificar que no está en la lista
curl http://127.0.0.1:8000/books
# → La lista ya no incluye el libro con id 3
Flujo completo en /docs
GET /books— Ver los 5 libros inicialesDELETE /books/3— Eliminar RayuelaGET /books/3— Debe retornar{"error": "Book not found"}GET /books— La lista tiene 4 libros
Probar DELETE con curl: flags útiles
| Flag | Uso |
|---|---|
-X DELETE | Especifica el método HTTP DELETE |
-v | Modo verbose — muestra status code y headers |
-s | Silencioso — suprime la barra de progreso |
Ejemplo con verbose para ver el status code:
curl -v -X DELETE http://127.0.0.1:8000/books/3
Verás en la respuesta la línea HTTP/1.1 200 OK (o el código que retorne tu endpoint). Para probar un ID inexistente:
curl -s -X DELETE http://127.0.0.1:8000/books/999
# → {"error":"Book not found"}
Variantes de respuesta en DELETE
| Estrategia | Body | Status | Uso típico |
|---|---|---|---|
| Confirmación con mensaje | {"message": "Book deleted", "id": 3} | 200 | APIs que quieren feedback explícito |
| Recurso eliminado | {"deleted": {...}, "message": "..."} | 200 | Cuando el cliente necesita el estado final |
| Sin body | — | 204 | Máxima adherencia a la spec HTTP |
| Solo ID | {"id": 3} | 200 | Minimalista |
Tu implementación actual (mensaje + id con status 200) es clara y práctica. El estándar 204 es más "puro" pero requiere manejo especial para errores (no puedes retornar {"error": "..."} con 204 — necesitas otro status para eso).
DELETE no recibe body
A diferencia de POST, PUT y PATCH, DELETE no usa Body(). Toda la información necesaria (el ID) viaja en la URL. Si necesitas confirmación adicional (ej: "¿estás seguro?"), eso se maneja en el cliente o con query parameters, no con body.
books.remove() vs books.pop()
books.remove(existing) — Elimina por valor. Necesitas el objeto exacto que está en la lista. Si tienes find_book() que retorna una referencia al mismo objeto en la lista, remove() funciona perfectamente.
books.pop(index) — Elimina por índice y retorna el elemento. Útil cuando necesitas el índice de otra forma (ej: en un loop con enumerate). Ambos modifican la lista in-place.
DELETE y relaciones entre recursos
En esta cápsula eliminas libros de una lista simple. En APIs reales, los recursos suelen tener relaciones: un libro puede tener reseñas, un usuario puede tener tareas. Al eliminar un recurso padre, hay que decidir qué pasa con los hijos:
- Eliminación en cascada — Al borrar un libro, se borran también sus reseñas.
- Restricción — No permitir DELETE si existen hijos (ej: no borrar un usuario con pedidos activos).
- Desvinculación — Los hijos quedan huérfanos o se asigna un valor por defecto.
Para tu lista en memoria, no hay relaciones; la eliminación es directa. Pero es bueno tener presente estos escenarios para futuros proyectos.
Eliminación y estado de la aplicación
Al eliminar un libro, la lista books se modifica. Cualquier request posterior a GET /books verá la lista actualizada. No hay "soft delete" ni "papelera" — el recurso desaparece de la memoria. En una base de datos real, podrías tener un campo deleted_at para soft delete; con lista en memoria, la eliminación es física.
Cómo verificar que la eliminación se aplicó
Después de un DELETE exitoso, un GET al mismo ID debe retornar un error (por ejemplo {"error": "Book not found"}). Un GET a la colección (/books) no debe incluir el recurso eliminado. Si tras DELETE el recurso sigue apareciendo, revisa que estés modificando la misma lista books que usan los endpoints GET — no una copia ni una variable local.
DELETE desde el cliente
A diferencia de POST, PUT y PATCH, DELETE no envía body. Con fetch en JavaScript: fetch("/books/3", { method: "DELETE" }). Con httpx en Python: httpx.delete("http://127.0.0.1:8000/books/3"). El ID va siempre en la URL. Si necesitas confirmación previa (ej: "¿Eliminar este libro?"), eso se maneja en la UI del cliente antes de disparar el request; el servidor asume que si recibió el DELETE, la confirmación ya ocurrió.
Herramientas para probar DELETE
- Swagger UI (/docs): Haz clic en el endpoint DELETE, "Try it out", introduce el
book_idy Execute. - curl:
curl -X DELETE http://127.0.0.1:8000/books/3 - Postman / Insomnia: Crea un request con método DELETE y la URL que incluya el ID.
- httpx (Python):
httpx.delete("...")para scripts de prueba automatizados.
En todos los casos, no envíes body; el método y la URL son suficientes.
Reintentos y idempotencia en el cliente
Como DELETE es idempotente, el cliente puede reintentar la operación sin miedo a efectos secundarios. Si una red inestable hace que la respuesta se pierda pero el servidor sí eliminó el recurso, un segundo DELETE al mismo ID dará "not found" — el estado final es correcto. Los clientes HTTP suelen reintentar automáticamente en timeouts; con DELETE eso es seguro.
CRUD recap: Las cinco operaciones
| Operación | Verbo | Ruta | Body | Status éxito |
|---|---|---|---|---|
| Listar | GET | /books | No | 200 |
| Obtener uno | GET | /books/{id} | No | 200 |
| Crear | POST | /books | Sí | 201 |
| Reemplazar | PUT | /books/{id} | Sí | 200 |
| Actualizar parcial | PATCH | /books/{id} | Sí | 200 |
| Eliminar | DELETE | /books/{id} | No | 200 (o 204) |
La misma ruta /books/{book_id} tiene comportamientos diferentes según el verbo. FastAPI los distingue automáticamente.
Código completo del CRUD
Tu app/main.py ahora tiene las seis operaciones:
from fastapi import FastAPI, Body
app = FastAPI()
books = [
{"id": 1, "title": "Cien años de soledad", "author": "Gabriel García Márquez", "year": 1967, "genre": "Realismo mágico"},
{"id": 2, "title": "Don Quijote", "author": "Miguel de Cervantes", "year": 1605, "genre": "Novela"},
{"id": 3, "title": "Rayuela", "author": "Julio Cortázar", "year": 1963, "genre": "Novela experimental"},
]
def generate_id():
if not books:
return 1
return max(b["id"] for b in books) + 1
def find_book(book_id: int):
return next((b for b in books if b["id"] == book_id), None)
@app.get("/books")
def get_books():
return books
@app.get("/books/{book_id}")
def get_book(book_id: int):
book = find_book(book_id)
if book is None:
return {"error": "Book not found"}
return book
@app.post("/books", status_code=201)
def create_book(book: dict = Body(...)):
book["id"] = generate_id()
books.append(book)
return book
@app.put("/books/{book_id}")
def update_book(book_id: int, book: dict = Body(...)):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
index = books.index(existing)
books[index] = {"id": book_id, **book}
return books[index]
@app.patch("/books/{book_id}")
def partial_update_book(book_id: int, updates: dict = Body(...)):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
for key, value in updates.items():
if key != "id":
existing[key] = value
return existing
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return {"message": "Book deleted", "id": book_id}
Errores comunes
Al implementar DELETE, estos errores aparecen con frecuencia:
- No ejecutar realmente la eliminación — Si solo llamas
find_book()y retornas un mensaje sin ejecutarbooks.remove(existing)obooks.pop(index), el recurso seguirá en la lista. El cliente cree que se eliminó pero un GET posterior lo devuelve. - Retornar body con status 204 — El status 204 No Content indica que la respuesta no tiene body. Si retornas
Response(status_code=204)pero intentas incluir un JSON, FastAPI o el cliente pueden comportarse de forma inesperada. Con 204, no envíes body. - Iterar y eliminar al mismo tiempo — Hacer
for book in books: ... books.remove(book)dentro del loop puede causar saltos de índice o comportamientos extraños. Usafind_book()para obtener la referencia y luegobooks.remove(existing)fuera del loop. - No verificar que el recurso existe — Si asumes que el ID siempre existe y haces
books.remove(existing)sin comprobarexisting is None, fallarás cuando el cliente envíe un ID inexistente. Siempre valida antes de operar. - Usar una lista distinta a la de GET — Si tu DELETE modifica una variable local o una copia de
booksen lugar de la lista global que usa GET, los cambios no se reflejarán. Asegúrate de quebookses la misma referencia en todos los endpoints.
Conexión con Proyecto
El endpoint DELETE que implementaste aquí es el patrón del To-Do List API (Módulo 6). En el Módulo 5 agregarás HTTPException(status_code=404) para "not found" y opcionalmente 204 para DELETE exitoso.
Troubleshooting
- DELETE retorna "Book not found" pero el libro existe — Verifica que usas
books.remove(existing)obooks.pop(index), no solofind_book(). La eliminación debe modificar la lista. - El libro sigue apareciendo tras DELETE — Asegúrate de que
remove()opop()se ejecuta. Revisa que la variablebookses la misma lista que usan GET y POST. - Mutation durante iteración — Si iteras con
for book in booksy hacesbooks.remove(book)dentro del loop, puede haber comportamiento inesperado. Mejor usafind_book()+books.remove(existing). - DELETE retorna body pero status es 204 — 204 no permite body. Usa 200 si quieres retornar confirmación.
- 405 Method Not Allowed en DELETE — Verifica que estás usando el verbo correcto. Con curl usa
-X DELETE. En/docs, el botón Execute enviará DELETE automáticamente. - El ID en la URL está mal formado — Si pasas un ID negativo o no numérico, FastAPI retornará 422 antes de llegar a tu función gracias a
book_id: int. Para IDs como UUIDs, cambiarías el tipo del path parameter según el formato esperado. - Reintentos y idempotencia — Si el cliente reintenta un DELETE tras un timeout, el recurso ya no existirá la segunda vez. Tu endpoint debe retornar "Book not found" en lugar de fallar. Ese comportamiento es correcto: DELETE es idempotente y el estado final (recurso ausente) es el mismo.
Ejercicios
Ejercicio 1: DELETE que retorna el recurso eliminado (Fácil)
Modifica el endpoint DELETE para que la respuesta incluya el libro eliminado.
Ver solución
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return {"message": "Book deleted", "deleted": existing}
Ejercicio 2: DELETE que retorna la lista actualizada (Fácil)
Modifica DELETE para que retorne la confirmación y la lista de libros restantes.
Ver solución
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return {
"message": "Book deleted",
"id": book_id,
"remaining_count": len(books),
"books": books
}
Ejercicio 3: DELETE con status 204 (Medio)
Modifica DELETE para que retorne 204 cuando tenga éxito. Para "not found", mantén un body con error (tendrás que usar 200 hasta el Módulo 5 para errores).
Ver solución
from fastapi import Response
@app.delete("/books/{book_id}")
def delete_book(book_id: int):
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return Response(status_code=204)
Ejercicio 4: DELETE con confirmación opcional (Medio)
Crea un endpoint que reciba un query parameter confirm=true. Si confirm no es true, retorna un error indicando que se necesita confirmación. Si es true, ejecuta el DELETE.
Ver solución
@app.delete("/books/{book_id}")
def delete_book(book_id: int, confirm: bool = False):
if not confirm:
return {"error": "Deletion requires confirm=true"}
existing = find_book(book_id)
if existing is None:
return {"error": "Book not found"}
books.remove(existing)
return {"message": "Book deleted", "id": book_id}
Uso: DELETE /books/3?confirm=true
Ejercicio 5: CRUD completo de productos (Difícil)
Implementa un CRUD completo para productos (id, name, price, in_stock) con las seis operaciones. Incluye DELETE que retorna el producto eliminado.
Ver solución
products = [
{"id": 1, "name": "Laptop", "price": 999.99, "in_stock": True},
{"id": 2, "name": "Mouse", "price": 29.99, "in_stock": True},
{"id": 3, "name": "Keyboard", "price": 79.99, "in_stock": False},
]
def find_product(prod_id: int):
return next((p for p in products if p["id"] == prod_id), None)
def generate_product_id():
return max(p["id"] for p in products) + 1 if products else 1
@app.get("/products")
def get_products():
return products
@app.get("/products/{product_id}")
def get_product(product_id: int):
p = find_product(product_id)
if p is None:
return {"error": "Product not found"}
return p
@app.post("/products", status_code=201)
def create_product(product: dict = Body(...)):
product["id"] = generate_product_id()
products.append(product)
return product
@app.put("/products/{product_id}")
def update_product(product_id: int, product: dict = Body(...)):
existing = find_product(product_id)
if existing is None:
return {"error": "Product not found"}
index = products.index(existing)
products[index] = {"id": product_id, **product}
return products[index]
@app.patch("/products/{product_id}")
def partial_update_product(product_id: int, updates: dict = Body(...)):
existing = find_product(product_id)
if existing is None:
return {"error": "Product not found"}
for k, v in updates.items():
if k != "id":
existing[k] = v
return existing
@app.delete("/products/{product_id}")
def delete_product(product_id: int):
existing = find_product(product_id)
if existing is None:
return {"error": "Product not found"}
products.remove(existing)
return {"message": "Product deleted", "deleted": existing}
Ejercicio 6: Flujo CRUD completo (Difícil)
Ejecuta manualmente (o escribe un script) este flujo y verifica que cada paso funciona. Usa tu API de libros del proyecto o la que hayas construido en las cápsulas anteriores:
- POST crear libro
- GET por ID para verificar
- PATCH para cambiar un campo
- GET por ID para verificar
- PUT para reemplazar todo
- DELETE
- GET por ID → debe retornar "not found"
- GET list → no debe incluir el libro eliminado
Ver solución
# 1. Crear
curl -s -X POST http://127.0.0.1:8000/books \
-H "Content-Type: application/json" \
-d '{"title": "El Principito", "author": "Saint-Exupéry", "year": 1943, "genre": "Novela"}'
# 2. Obtener (asume id 4)
curl -s http://127.0.0.1:8000/books/4
# 3. PATCH
curl -s -X PATCH http://127.0.0.1:8000/books/4 \
-H "Content-Type: application/json" \
-d '{"genre": "Novela corta"}'
# 4. Verificar PATCH
curl -s http://127.0.0.1:8000/books/4
# 5. PUT
curl -s -X PUT http://127.0.0.1:8000/books/4 \
-H "Content-Type: application/json" \
-d '{"title": "El Principito", "author": "Antoine de Saint-Exupéry", "year": 1943, "genre": "Novela corta"}'
# 6. DELETE
curl -s -X DELETE http://127.0.0.1:8000/books/4
# 7. Verificar eliminación
curl -s http://127.0.0.1:8000/books/4
# {"error":"Book not found"}
# 8. Listar
curl -s http://127.0.0.1:8000/books
Recapitulación del módulo: CRUD completo
Tras completar las cápsulas 02 a 05, tienes:
- GET /books — Lista todos
- GET /books/{id} — Obtiene uno por ID
- POST /books — Crea con Body() y status 201
- PUT /books/{id} — Reemplaza completo
- PATCH /books/{id} — Actualiza parcial
- DELETE /books/{id} — Elimina con confirmación
Funciones auxiliares: find_book, generate_id. Almacenamiento: lista en memoria. El proyecto de la cápsula 06 integra todo en una API de libros lista para extender en el Módulo 3.
Resumen
- DELETE elimina un recurso por su ID
- No recibe body — solo el ID en la URL
- Status 200 con body para confirmación; 204 para respuesta sin contenido
- Usa
find_book()+books.remove(existing)para eliminar - Verifica la eliminación con GET por ID y GET list
- CRUD completo: GET (list + detail), POST, PUT, PATCH, DELETE
Próxima cápsula: Proyecto CRUD básico — Integrarás todo en una API de libros completa con rúbrica y verificación.
Recursos Adicionales
- FastAPI - Response Status Code - Configurar status codes
- HTTP DELETE - MDN - Especificación del método DELETE
- HTTP Status 204 - MDN - Significado del status 204 No Content
- REST API - HTTP Methods - Convenciones REST para DELETE
- RFC 7231 - DELETE - Especificación oficial HTTP para DELETE
- REST API Tutorial - DELETE - Buenas prácticas para eliminación de recursos
Módulo 2, Cápsula 05 — FastAPI Fundamentals Guide