Nautobot Apps - Lab 07: Extending the REST API¶
For this lab, we'll extend Nautobot's REST API to facilitate basic create, read, update, and delete (CRUD) operations for our MaintenanceNotice model. We'll rely on the Django REST Framework (DRF) for this, which is a third-party Django app that Nautobot employs for its own REST API.
- Nautobot Apps - Lab 07: Extending the REST API
- Task 1: Create a REST API Token
- Task 2: Introduce a Custom REST API Endpoint
- Step 2-1 - Create the API Folder and Files
- Step 2-2 - Create the Serializer
- Step 2-3 - Create the ViewSet
- Step 2-4 - Create the URL path
- Step 2-5 - Check your API
- Step 2-6 - Display the User's Name
- Step 2-7 - Verifying the
serializers.pyfile - Step 2-8 - Verifying the API Results
- Step 2-9 - Interact with the API using cURL
- Step 2-10 - Sending a POST command to the REST API with cURL
- Step 2-11 - Add the Create-By User to Maintenance Notices
- Step 2-12 - Verifying the Change
- Task 3: Enabling Filtering of API Results
- Task 4: Enabling GraphQL access
Task 1: Create a REST API Token¶
Nautobot uses token-based authentication for its REST API, so let's start off by creating a token. The token will be passed in an authentication header with each request.
Step 1-0 - Prepare for the Lab¶
For this and all remaining labs, it is advisable to open two terminal windows, one for running the Nautobot development server, and another for executing various commands.
The steps in this lab require that you have a terminal open and have switched to the nautobot user. Verify your username with the whoami command and switch user to nautobot if needed.
ntc@nautobot:~$ whoami
ntc
ntc@nautobot:~$ sudo -iu nautobot
nautobot@nautobot:~$ whoami
nautobot
nautobot@nautobot:~$
If you have the Nautobot development server running from a previous lab, please continue to the next step. Otherwise we need to start the server with the command nautobot-server runserver 0.0.0.0:8080 --insecure.
nautobot@ntc-nautobot-apps:~$ nautobot-server runserver 0.0.0.0:8080 --insecure
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
April 24, 2022 - 05:19:10
Django version 3.1.14, using settings 'nautobot_config'
Starting development server at http://0.0.0.0:8080/
Quit the server with CONTROL-C.
Step 1-1 - Create a Token¶
In your web browser, ensure you are logged into the Nautobot server as the admin user. Navigate to the user control panel by clicking the user icon in the top right corner of the page and selecting "Profile."

On the next page, click "API Tokens."

Step 1-2 - Creating a Token¶
Click "Add a token" to create a new API token.

Note: There may already be a token populated in this screen. You can ignore it, the point of the exercise is to learn to create a token.
Nautobot will present a form for creating a new REST API token. Leave the key field blank so that Nautobot generates a random token for us. You'll also see options to toggle write access and expiration time: these fields can also be left in their default states. Click the "create" button to generate a token.

Step 1-3 - Add a Token to Environment¶
After the new token has been created, click the "Copy" button next to the new token value to copy it to the clipboard.

Then, set it as a variable in your terminal so that it can be easily referenced when forming API requests. For example:
nautobot@nautobot:~$ export TOKEN="87e1ae17a60c6f6b7309c76a6fb79e4e2853af29"
nautobot@nautobot:~$ echo $TOKEN
87e1ae17a60c6f6b7309c76a6fb79e4e2853af29
If you need to recover your token at a future point, just return to the Nautobot user control panel.
Task 2: Introduce a Custom REST API Endpoint¶
There are three core components which comprise a REST API endpoint: serializer, view, and router.
Step 2-1 - Create the API Folder and Files¶
We'll create a subdirectory within the plugin root for all of our REST API-related code named api/. Additionally, we will create some empty files that we will populate during this lab.
nautobot@nautobot:~$ mkdir -p ~/plugin/maintenance_notices/api
nautobot@nautobot:~$ cd ~/plugin/maintenance_notices/api
nautobot@nautobot:~/plugin/maintenance_notices/api$ touch __init__.py serializers.py urls.py views.py
Step 2-2 - Create the Serializer¶
Edit the maintenance_notices/api/serializers.py file.
Nautobot makes creating serializers from models extremely convenient. We just need to subclass nautobot.core.api.ValidatedModelSerializer and specify our model and the fields we want to include.
from nautobot.core.api.serializers import ValidatedModelSerializer
from maintenance_notices.models import MaintenanceNotice
class MaintenanceNoticeSerializer(ValidatedModelSerializer):
class Meta:
model = MaintenanceNotice
fields = ("id", "start_time", "end_time", "duration", "devices", "created_by")
Step 2-3 - Create the ViewSet¶
Next, we need a viewset to handle our endpoint functions. Nautobot provides a ModelViewSet, which can handle all the common operations. Edit the maintenance_notices/api/views.py file and import it along with our model and serializer. Then, create a subclass to tie them all together.
from nautobot.core.api.views import ModelViewSet
from maintenance_notices.models import MaintenanceNotice
from .serializers import MaintenanceNoticeSerializer
class MaintenanceNoticeViewSet(ModelViewSet):
queryset = MaintenanceNotice.objects.all()
serializer_class = MaintenanceNoticeSerializer
The queryset attribute defines the base model and queryset for the endpoint, whereas the serializer contains the logic for rendering instances of the model as JSON.
Step 2-4 - Create the URL path¶
To make our viewset accessible, we need to create a URL router. This is similar in concept to how we created URL patterns for our regular UI views, but works a bit differently. First, edit maintenance_notices/api/urls.py and setup our imports.
from nautobot.core.api.routers import OrderedDefaultRouter
from maintenance_notices.api import views
Next we want to instantiate Nautobot's OrderedDefaultRouter class and assign it to the name router, which is an object that routes the incoming URL requests to the right code. Additionally we can register a path to the MaintenanceNoticeViewSet that we setup in the previous step, and finally, we set our urlpatterns to the urls attribute of the router object. This last line exposes the generated URL patterns.
router = OrderedDefaultRouter()
router.register("", views.MaintenanceNoticeViewSet)
urlpatterns = router.urls
Step 2-5 - Check your API¶
Keep in mind that the nautobot-server will only monitor changes in Python file that it loaded when we started the server. It is unaware of the files we just created, so you will need to restart your server in order to see the page we just created.
This should cause your server to reload:
In your browser address bar, add this path to your server address: /api/plugins/maintenance_notices/. You should see a list of maintenance notices, serialized as JSON. At this point, we have a fully-functional REST API endpoint that can create, modify, and delete maintenance notices!

Step 2-6 - Display the User's Name¶
Inspecting the output above, we see that the created_by field specifies an UUID, rather than a username. This is the hexadecimal ID associated with the related User object. If we would rather see the user's name, we can override the automatically-generated serializer field with StringRelatedField to output a username instead.
Edit the serializers.py file and add the following import at the top of the file:
Now add the line created_by = StringRelatedField(read_only=True) just below the class definition.
class MaintenanceNoticeSerializer(ValidatedModelSerializer):
created_by = StringRelatedField(read_only=True)
...
Note that the field must be set to read_only, since we don't want to allow its modification. Now when we retrieve a maintenance notice via the REST API, the serialized representation will contain the user's name.
Save the file.
Step 2-7 - Verifying the serializers.py file¶
Here is what your complete serializers.py file should look like.
from nautobot.core.api.serializers import ValidatedModelSerializer
from rest_framework.serializers import StringRelatedField
from maintenance_notices.models import MaintenanceNotice
class MaintenanceNoticeSerializer(ValidatedModelSerializer):
created_by = StringRelatedField(read_only=True)
class Meta:
model = MaintenanceNotice
fields = ("id", "start_time", "end_time", "duration", "devices", "created_by")
Step 2-8 - Verifying the API Results¶
Refresh your browser and you should now see the user name displayed in the JSON output.

Step 2-9 - Interact with the API using cURL¶
You can use the curl command to send a request to the server. Let's send an HTTP GET command to our REST API. With the development server still running, open a new terminal.
curl -X GET -H "Authorization: Token $TOKEN" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8080/api/plugins/maintenance_notices/
Notice that you should receive text that looks the same as what we received in the browser.
nautobot@ntc-nautobot-apps:~/plugin/maintenance_notices$ curl -X GET -H "Authorization: Token $TOKEN" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8080/api/plugins/maintenance_notices/
{
"count": 6,
"next": null,
"previous": null,
"results": [
{
"id": "28984de7-92b7-4393-b50e-8865652d7383",
"start_time": "2022-03-15T12:00:00Z",
"end_time": "2022-03-15T13:00:00Z",
"duration": 60,
"devices": [
"ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6"
],
"created_by": "admin",
"display": "2022-03-15 12:00 (60 minutes)"
},
...
Step 2-10 - Sending a POST command to the REST API with cURL¶
In both cases, there's still a problem regarding the user association. If you haven't already, try creating a new MaintenanceNotice by sending a POST HTTP request to the endpoint.
Note: The command below assumes that you set the
TOKENvariable to your REST API token in the previous task and that the device ID listed in thedevicesdata is a valid ID for your instance. You can copy/paste one from the results you see in your browser or from the previouscurlcommand.
Navigate to the Devices menu in Nautobot UI and click on any given device in your inventory. Once the page loads, grab the URL and extract the Primary Key from the device.
In the example above, we are interested in ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6. Add this to a list of devices in our payload.
curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://localhost:8080/api/plugins/maintenance_notices/ \
--data '{
"start_time": "2022-12-31T00:00:00Z",
"duration": 60,
"devices": ["ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6"]
}'
While the values might differ, the result should be of the same format as this:
{
"id": "ca6fb81a-50bf-40ba-9647-12ab76b9380b",
"start_time": "2022-12-31T00:00:00Z",
"end_time": "2022-12-31T01:00:00Z",
"duration": 60,
"devices": [
"ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6"
],
"created_by": null,
"display": "2022-12-31 00:00 (60 minutes)"
}
Notice that although a new maintenance notice is created, the created_by field is set to None (or null in JSON). Recall that in a previous lab we needed to add a custom post() method on the MaintenanceNoticeCreateView to record the currently authenticated user. We need to do something similar for our REST API view set.
Step 2-11 - Add the Create-By User to Maintenance Notices¶
Edit the maintenance_notices/api/views.py file. Let's extend the MaintenanceNoticeViewSet class and override its perform_create() method:
class MaintenanceNoticeViewSet(ModelViewSet):
...
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
This sets created_by to the authenticated user when the serializer is being saved. Now when a maintenance notice is created via the REST API, the authenticated user will be recorded.
The completed plugin/maintenance_notices/api/views.py file should look like this:
from nautobot.core.api.views import ModelViewSet
from maintenance_notices.models import MaintenanceNotice
from .serializers import MaintenanceNoticeSerializer
class MaintenanceNoticeViewSet(ModelViewSet):
queryset = MaintenanceNotice.objects.all()
serializer_class = MaintenanceNoticeSerializer
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
Step 2-12 - Verifying the Change¶
We can verify this by issuing a second API request:
curl -X POST \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://localhost:8080/api/plugins/maintenance_notices/ \
--data '{
"start_time": "2022-12-31T00:00:00Z",
"duration": 60,
"devices": ["ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6"]
}'
You see the user name in the received JSON output:
{
"id": "e5dfcfc0-f936-48c6-b9f3-85977b26b47b",
"start_time": "2022-12-31T00:00:00Z",
"end_time": "2022-12-31T01:00:00Z",
"duration": 60,
"devices": [
"ca3a9003-b6ac-4e9a-b3f9-3f7e402445d6"
],
"created_by": "admin",
"display": "2022-12-31 00:00 (60 minutes)"
}
Task 3: Enabling Filtering of API Results¶
To extend the utility of our new REST API endpoint, we'll create a FilterSet using the django-filter library. This will allow us to filter API results by appending certain parameters to the request URL.
Step 3-1 - Create the filters file¶
Create filters.py in the plugin project path (not the api/ subdirectory).
nautobot@nautobot:~/plugin/maintenance_notices/api$ cd ~/plugin/maintenance_notices/
nautobot@nautobot:~/plugin/maintenance_notices$ touch filters.py
nautobot@nautobot:~/plugin/maintenance_notices$
Step 3-2 - Create the FilterSet¶
Now edit the filters.py file that you just created. Create a filterset for the MaintenanceNotice model by subclassing FilterSet from django-filters and setting the model and fields on which we want to filter.
from nautobot.utilities.filters import BaseFilterSet
from .models import MaintenanceNotice
class MaintenanceNoticeFilterSet(BaseFilterSet):
class Meta:
model = MaintenanceNotice
fields = ("id", "start_time", "end_time", "duration", "devices", "created_by")
This creates a set of filters automatically for the specified model fields.
Step 3-3 - Add the FilterSet to the View¶
Open the api/views.py in your editor. We need to import the MaintenanceNoticeFilterSet that we just created. Add this line into your other imports.
Now add the filterset_class = MaintenanceNoticeFilterSet into your MaintenanceNoticeViewSet, between the queryset and the serializer_class.
class MaintenanceNoticeViewSet(ModelViewSet):
queryset = MaintenanceNotice.objects.all()
filterset_class = MaintenanceNoticeFilterSet
serializer_class = MaintenanceNoticeSerializer
...
Notice that we have positioned the filterset_class after the queryset. The queryset contains all available objects, the filterset_class filters the objects to those specified, and serializer_class then serializes those objects to JSON format.
Step 3-4 - Verify the api/views.py file¶
Verify your maintenance_notices/api/views.py file looks like the one below.
from nautobot.core.api.views import ModelViewSet
from maintenance_notices.filters import MaintenanceNoticeFilterSet
from maintenance_notices.models import MaintenanceNotice
from .serializers import MaintenanceNoticeSerializer
class MaintenanceNoticeViewSet(ModelViewSet):
queryset = MaintenanceNotice.objects.all()
filterset_class = MaintenanceNoticeFilterSet
serializer_class = MaintenanceNoticeSerializer
def perform_create(self, serializer):
serializer.save(created_by=self.request.user)
Save the file.
Step 3-5 - Test the FilterSet¶
With our filterset applied, we can now filter API results by appending query parameters to the URL. For example, we can retrieve only maintenance notices that are exactly 120 minutes long by fetching /api/plugins/maintenance_notices/notices/?duration=120.
Here are some other queries to try:
?start_time=YYYY-MM-DD%20HH:MM:SS, whereYYYY-MM-DDis the starting date%20is a URL encoded space andHH:MM:SSis the time.?devices=ID, whereIDis the ID of an assigned device?created_by=ID, whereIDis the ID of the assigned user
curl -X GET \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://localhost:8080/api/plugins/maintenance_notices/?duration=120
Note: The exact results of the above queries will depend on the maintenance notices you've created.
Note: If your results are not being filtered as you would expect, check the order in your
/maintenance_notices/api/views.pyfile. Thefilterset_classshould come after thequerysetassignment and before theserializer_classassignment.
Step 3-6 - Create a Custom Filter¶
The automatically-generated filters match only on exact values, but we can add our own filters to make more complex queries. For example, what if we want to retrieve only active maintenance notices (as we do when viewing a device in the Nautobot UI)?
Note: Our goal here is to retrieve notices that are current or pending, and to exclude notices that have occurred in the past. active by this definition excludes expired notices and is not limited to those whose maintenance window is within the current time.
Let's add a filter named active to MaintenanceNoticeFilterSet, which will call a custom method to alter the queryset. (We'll also need to import Django's timezone utility to get the current time, so go ahead and do that now too.)
Edit the filters.py file and add these two import statements above your existing imports.
Add the active attribute right below the class definition:
class MaintenanceNoticeFilterSet(BaseFilterSet):
active = django_filters.BooleanFilter(method='filter_active')
After the filterset's Meta class, create the filter_active() method:
class MaintenanceNoticeFilterSet(BaseFilterSet):
...
class Meta:
...
def filter_active(self, queryset, name, value):
if value:
return queryset.filter(end_time__gt=timezone.now())
else:
return queryset.exclude(end_time__gt=timezone.now())
This method examines the provided value, which will be either true or false because we've declared a boolean filter. If true, the queryset is filtered to return only maintenance notices with an end_time in the future. If false, these notices are excluded.
Note: It is equally valid to filter by
end_time__lte=timezone.now()whenvalueis false. The approach you choose often comes down to personal preference. However, when using this approach always ensure that one condition or the other handles equal values in addition to those which are greater or less.
Save the file.
Step 3-7 - Test a Custom Filter¶
Now, when filtering using ?active=true, the endpoint will return only active maintenance notices, and filtering with ?active=false will return only inactive notices.
Let's try our filter:
curl -X GET \
-H "Authorization: Token $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json; indent=4" \
http://localhost:8080/api/plugins/maintenance_notices/?active=true
This is only a brief example of the flexibility afforded by django-filter. Students are strongly encouraged to review the full documentation to discover all that the package offers.
Task 4: Enabling GraphQL access¶
Step 4-1 - Add Extra Features to model¶
Once you have your REST API serializers built, it is trivial to add GraphQL support. All we need to do is decorate our model with the @extras_features decorator, and pass in "graphql" as an argument.
Edit models.py and add these lines just above the class definition. The last line, class MaintenanceNotice(BaseModel):, remains unchanged.
from nautobot.extras.utils import extras_features
@extras_features("graphql")
class MaintenanceNotice(BaseModel):
from nautobot.extras.utils import extras_features
@extras_features("graphql")
class MaintenanceNotice(BaseModel):
Step 4-2 - Test GraphQL access¶
From the Nautobot UI, navigate to the bottom right-hand corner, where you will see a series of links, including GraphQL. Click the GraphQL link to access the GraphQL interactive development environment.

Note: The first time you click this page it will take some time to generate the content. You can see the progress in the terminal where you are running the server.
Try a simple query like:
The above query collects maintenance_notice objects, and returns the fields specified withing the curly braces (start_time, end_time, and devices). The default field returned for any model object is the id field, so here we are requesting that GraphQL return the name field for each device.
Note that we can further nest fields, as in the next example, where we are including the device's site field, and specifically the name of that site.

Note: When specifying top-level models in your query, always use the plural form of the model, eg.
maintenance_noticesvs.maintenance_notice.
Step 4-3 - Commit Your Changes¶
Remember to commit your changes to git:
nautobot@nautobot:~$ cd ~/plugin/maintenance_notices
nautobot@nautobot:~/plugin/maintenance_notices$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: __init__.py
modified: navigation.py
modified: urls.py
modified: views.py
Untracked files:
(use "git add <file>..." to include in what will be committed)
api/
filters.py
template_content.py
templates/maintenance_notices/device_notices.html
no changes added to commit (use "git add" and/or "git commit -a")
nautobot@nautobot:plugin/maintenance_noices$ git add -A
nautobot@nautobot:plugin/maintenance_notices$ git commit -m "Completed Lab 7."
This lab is now complete. Check your work against the solution guide before proceeding with the next lab.