Endpoint anidado para citas en Django REST

Resumen

Crear un endpoint anidado en Django REST Framework te permite estructurar URLs que reflejan relaciones reales entre recursos, como un doctor y sus citas médicas. Aquí aprendes a construir un endpoint que liste y cree appointments asociados a un doctor específico, siguiendo principios REST y reutilizando el id del recurso padre.

Por qué usar un endpoint anidado para crear citas

Cuando un recurso depende de otro, anidar la URL aclara la jerarquía y simplifica la lógica del backend. En este caso, una cita siempre pertenece a un doctor, así que la URL /doctors/{id}/appointments/ comunica esa relación de inmediato.

La idea es que un GET a esa ruta devuelva la lista de citas de ese doctor, y un POST cree una nueva cita usando el id del doctor que ya viene en la URL. Esto evita que el cliente pueda manipular el doctor desde el body, algo clave para mantener la integridad de los datos.

¿Qué es un endpoint anidado en REST? Es una URL que expone un recurso hijo bajo el contexto de un recurso padre, por ejemplo /doctors/3/appointments/. Sirve cuando el hijo no tiene sentido fuera del padre.

Cómo definir la action appointments en el ViewSet

Django REST Framework permite agregar rutas personalizadas a un ModelViewSet mediante el decorador @action. Para esta funcionalidad necesitas declarar que la acción acepta tanto GET como POST, y marcarla como vista de detalle porque depende del id de un doctor.

Dentro del viewset de doctores defines un método appointments decorado con @action(detail=True, methods=['GET','POST']). El parámetro detail=True indica que la URL incluirá el pk del doctor, mientras que la lista de métodos habilita ambas operaciones en la misma ruta.

Dentro del método obtienes el doctor con self.get_object(), que aprovecha el queryset del viewset para recuperar la instancia que coincide con el pk recibido. A partir de ahí, ramificas el comportamiento según request.method.

Cómo manejar POST para crear una cita

Para crear el appointment, importas AppointmentSerializer desde bookings.serializers y construyes un diccionario nuevo basado en request.data. La razón de copiar la data en lugar de usarla directamente es forzar el id del doctor desde la URL, así el cliente no puede sobrescribirlo.

Los pasos concretos son:

  • Copiar request.data en una variable data.
  • Asignar data['doctor'] = doctor.id, ya que Django REST enlaza relaciones por id.
  • Instanciar AppointmentSerializer(data=data) pasando el parámetro nombrado data para que el serializer entienda que se trata de información a validar, no de una instancia existente.
  • Llamar a serializer.is_valid(raise_exception=True) para devolver un error legible si algo no cumple las reglas.
  • Ejecutar serializer.save() para persistir la cita.
  • Retornar Response(serializer.data, status=status.HTTP_201_CREATED).

El módulo status se importa desde rest_framework y expone constantes como HTTP_201_CREATED, que hacen el código más legible que escribir el número directamente.

Cómo manejar GET para listar las citas del doctor

Para el GET, filtras el modelo Appointment por el doctor recibido. Importas el modelo con from bookings.models import Appointment y ejecutas Appointment.objects.filter(doctor=doctor).

Luego serializas la lista con AppointmentSerializer(appointments, many=True). El argumento many=True es indispensable cuando pasas un queryset o lista de instancias, porque le indica al serializer que debe iterar y producir un arreglo JSON. Finalmente retornas Response(serializer.data), sin necesidad de especificar el estado porque 200 es el valor por defecto.

Cómo evitar que el formulario muestre los campos del doctor

Un detalle que aparece al probar el endpoint en la interfaz gráfica de Django REST es que el formulario de POST muestra los campos del doctor en lugar de los de la cita. Esto pasa porque el viewset infiere el serializer del recurso principal.

La solución es declarar serializer_class=AppointmentSerializer dentro del decorador @action. Con ese argumento, el browsable API renderiza los campos correctos: fecha, hora, notas, estado, paciente y doctor.

¿Por qué usar serializer_class dentro de @action? Porque la acción crea un recurso distinto al del viewset padre. Sin esa línea, la interfaz muestra los campos del doctor en vez de los de la cita.

Cómo probar el endpoint anidado en el navegador

Una vez levantado el servidor de desarrollo y autenticado, navegas al detalle de un doctor, por ejemplo /doctors/3/. En la barra superior aparecen las extra actions, incluida appointments.

Al entrar verás dos comportamientos:

  • Con GET obtienes la lista de citas filtradas por ese doctor.
  • Con POST se despliega el formulario para crear una cita nueva, donde defines fecha, hora, notas como urgente, estado en espera, y seleccionas el paciente. El doctor queda fijado por la URL.

Al enviar el POST, la respuesta devuelve el objeto creado con estado 201. Si recargas con GET, la nueva cita aparece en la lista junto a las anteriores.

Con esta estructura tienes un viewset anidado funcional que respeta REST, protege la relación con el doctor y aprovecha el ORM de Django para filtrar. ¿Te animas a extender la misma acción para borrar una cita por su id? Cuéntame en los comentarios cómo lo implementarías.