# How to Add a New API Endpoint to Plane's Django Backend (apps/api)

> Easily add a new API endpoint to Plane's Django backend by creating serializers, implementing ViewSets, and registering routes. Learn the steps for apps api.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-18

---

**To add a new API endpoint to Plane's Django backend, create a DRF serializer in [`apps/api/plane/api/serializers.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/serializers.py), implement a ViewSet in [`apps/api/plane/api/views.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/views.py), and register the route in [`apps/api/plane/api/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/urls.py) under the `api/v1/` prefix.**

Plane is an open-source project management tool built on Django and Django REST Framework (DRF). All public API routes are mounted under `api/v1/` and defined within the `apps/api/plane/api/` package, following standard DRF conventions for serializers, viewsets, and routers.

## Create a Django REST Framework Serializer

Start by defining a serializer that converts your model instances to JSON and validates incoming data. Create or edit [`apps/api/plane/api/serializers.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/serializers.py) to add a `ModelSerializer` for the target model.

```python

# apps/api/plane/api/serializers.py

from rest_framework import serializers
from plane.models import MyModel  # Replace with your actual model

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ("id", "name", "created_at", "updated_at")

```

Plane stores its DRF serializers in this centralized location to maintain consistency with existing endpoints for issues, labels, and cycles. This structure allows the test suite to discover serializers automatically and keeps the codebase organized.

## Implement the ViewSet Business Logic

Next, create a view that contains the business logic for your endpoint. Add a `ModelViewSet` to [`apps/api/plane/api/views.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/views.py) to automatically provide `list`, `retrieve`, `create`, `update`, and `destroy` actions.

```python

# apps/api/plane/api/views.py

from rest_framework import viewsets, permissions
from plane.models import MyModel
from .serializers import MyModelSerializer

class MyModelViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MyModelSerializer
    permission_classes = [permissions.IsAuthenticated]

```

This pattern mirrors existing viewsets in the Plane repository, such as the issue viewset. Using `ModelViewSet` eliminates boilerplate code while maintaining full CRUD functionality. Adjust the `permission_classes` list to match your security requirements—Plane typically uses `IsAuthenticated` or custom permission classes defined in [`apps/api/plane/api/permissions.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/permissions.py).

## Register the Route in the API Router

Expose the new endpoint by registering the ViewSet with a DRF router in [`apps/api/plane/api/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/urls.py).

```python

# apps/api/plane/api/urls.py

from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import MyModelViewSet

router = DefaultRouter()
router.register(r"my-models", MyModelViewSet, basename="my-model")

urlpatterns = [
    path("", include(router.urls)),
]

```

The top-level URL configuration in [`apps/api/plane/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/urls.py) already includes this module at the `api/v1/` prefix:

```python

# apps/api/plane/urls.py

urlpatterns = [
    path("api/v1/", include("plane.api.urls")),
]

```

Because the router attaches to an empty string, your final endpoint URLs become:

- `GET /api/v1/my-models/` (list)
- `POST /api/v1/my-models/` (create)
- `GET /api/v1/my-models/<id>/` (retrieve)
- `PUT /api/v1/my-models/<id>/` (update)
- `DELETE /api/v1/my-models/<id>/` (destroy)

## Add Permissions, Filtering, and Tests

For production-ready endpoints, extend the basic implementation with these additional components:

- **Custom Permissions**: Create permission classes in [`apps/api/plane/api/permissions.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/permissions.py) and reference them in your ViewSet's `permission_classes` attribute to enforce workspace-level or project-level access controls.
- **Query Filtering**: Implement `django-filter` FilterSets in [`apps/api/plane/api/filters.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/filters.py), then attach them to your ViewSet using `filterset_class = MyModelFilter` to enable query-string filtering.
- **Unit Tests**: Write tests in `apps/api/plane/tests/unit/` following the pattern of existing files like [`test_issue.py`](https://github.com/makeplane/plane/blob/main/test_issue.py). Use `pytest.mark.django_db` and DRF's `APIClient` to test authentication, serialization, and CRUD operations.

## Summary

- **Serializers** live in [`apps/api/plane/api/serializers.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/serializers.py) and handle model-to-JSON conversion using `ModelSerializer`.
- **ViewSets** reside in [`apps/api/plane/api/views.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/views.py) and implement business logic through DRF's `ModelViewSet` class.
- **URL Routing** occurs in [`apps/api/plane/api/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/urls.py) using `DefaultRouter`, with the project-wide [`apps/api/plane/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/urls.py) mounting routes at `/api/v1/`.
- **Authentication** follows Plane's existing patterns using `permission_classes` on ViewSets.
- **Testing** should mirror existing unit tests in `apps/api/plane/tests/unit/` using pytest and the Django database marker.

## Frequently Asked Questions

### What file structure does Plane use for API endpoints?

Plane organizes its Django REST Framework components within `apps/api/plane/api/`, with [`serializers.py`](https://github.com/makeplane/plane/blob/main/serializers.py) for data validation, [`views.py`](https://github.com/makeplane/plane/blob/main/views.py) for business logic, and [`urls.py`](https://github.com/makeplane/plane/blob/main/urls.py) for routing. The project-wide URL configuration in [`apps/api/plane/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/urls.py) includes these routes under the `api/v1/` namespace, ensuring all endpoints follow a consistent versioning scheme.

### How do I handle permissions for new endpoints in Plane?

Add custom permission classes to [`apps/api/plane/api/permissions.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/permissions.py) if you need workspace-specific or role-based access control. Then import and reference these classes in your ViewSet's `permission_classes` list. For standard authentication, use `permissions.IsAuthenticated` from Django REST Framework to require valid user sessions.

### Where should I write tests for new API endpoints?

Create test files in `apps/api/plane/tests/unit/` following the naming convention `test_<model>.py`. Use `pytest.mark.django_db` to enable database access and DRF's `APIClient` to simulate HTTP requests. Run the test suite using `docker compose -f docker-compose-test.yml run --rm api-tests pytest -m unit` from the repository root.

### How do I access the new endpoint after deployment?

Once registered, your endpoint becomes available at `https://<your-domain>/api/v1/<your-route>/` based on the router prefix defined in [`apps/api/plane/api/urls.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/urls.py). Ensure your frontend or API client includes proper authentication headers (Bearer tokens or session cookies) as required by the ViewSet's permission classes.