├── .gitignore ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ryan Transfiguracion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-rest-api-cheatsheet 2 | 3 | A simple cheatsheet for a Django REST API backend. 4 | 5 | ## Development Environment (Mac OSX) 6 | 7 | 1. Install Python 3 8 | 9 | ``` 10 | $ brew install python3 11 | ``` 12 | 13 | 2. Install pipenv 14 | 15 | ``` 16 | $ pip3 install pipenv 17 | ``` 18 | 19 | ## Initial Project Setup 20 | 21 | ``` 22 | $ mkdir projdir && cd projdir 23 | $ pipenv install django==2.1 24 | ``` 25 | 26 | ## NOTE: pip 18.1 is buggy! Current fix (immediately after the previous step) is this: 27 | 28 | ``` 29 | $ pip3 install pipenv 30 | $ pipenv run pip install pip==18.0 31 | $ pipenv install 32 | ``` 33 | 34 | ## Initial Project Setup (Continued) 35 | 36 | ``` 37 | $ pipenv install pylint 38 | $ pipenv shell 39 | (projdir) $ django-admin startproject my_project . 40 | (projdir) $ python manage.py migrate 41 | (projdir) $ python manage.py createsuperuser 42 | (projdir) $ python manage.py runserver 43 | ``` 44 | 45 | Suggestion: Change the URL of the admin site in `my_project/urls.py`. 46 | 47 | ## Adding a Django app to the project 48 | 49 | ``` 50 | (projdir) $ python manage.py startapp products 51 | ``` 52 | 53 | 2. Add the new app to `INSTALLED_APPS` list in `my_project/settings.py`. 54 | 55 | ```python 56 | # my_project/settings.py 57 | INSTALLED_APPS = [ 58 | 'django.contrib.admin', 59 | 'django.contrib.auth', 60 | 'django.contrib.contenttypes', 61 | 'django.contrib.sessions', 62 | 'django.contrib.messages', 63 | 'django.contrib.staticfiles', 64 | # Local 65 | 'products.apps.ProductsConfig', 66 | ] 67 | ``` 68 | 69 | ## Creating a Model for the project 70 | 71 | 1. Go to `products/models.py` and make model: 72 | 73 | ```python 74 | # products/models.py 75 | from django.db import models 76 | from django.contrib.auth.models import User # if you need user here... 77 | 78 | class Product(models.Model): 79 | created_by = models.ForeignKey(User, on_delete=models.CASCADE) 80 | name = models.CharField(max_length=250) 81 | description = model.TextField() 82 | image = models.ImagesField(uploaf_to='dir/of/image/') 83 | created_at = models.DateTimeField(auto_now_add=True) 84 | 85 | def __str__(self): 86 | return self.name 87 | ``` 88 | 89 | (See https://docs.djangoproject.com/en/2.1/ref/models/fields/ for model fields.] 90 | 91 | 2. Create migration file (make sure you add the app name at the end). 92 | 93 | ``` 94 | (projdir) $ python manage.py makemigrations products 95 | (projdir) $ python manage.py migrate 96 | ``` 97 | 98 | 3. Register the model in `products/admin.py`. 99 | 100 | ```python 101 | # products/admin.py 102 | from django.contrib import admin 103 | from .models import Product # Added in this step 104 | 105 | admin.site.register(Product) # Added in this step 106 | ``` 107 | 108 | ## Setting up Django REST Framework 109 | 110 | ```unix 111 | (projdir) $ pipenv install djangorestframework==3.8.2 112 | ``` 113 | 114 | 2. Add to `INSTALLED_APPS` and add `REST_FRAMEWORK` object in `my_project/settings.py`: 115 | 116 | ```python 117 | # my_project/settings.py 118 | ... 119 | INSTALLED_APPS = [ 120 | 'django.contrib.admin', 121 | 'django.contrib.auth', 122 | 'django.contrib.contenttypes', 123 | 'django.contrib.sessions', 124 | 'django.contrib.messages', 125 | 'django.contrib.staticfiles', 126 | 127 | # 3rd-party 128 | 'rest_framework', # Added in this step 129 | # Local 130 | 'products', 131 | ] 132 | 133 | REST_FRAMEWORK = { # Added in this step 134 | 'DEFAULT_PERMISSION_CLASSES': [ 135 | 'rest_framework.permissions.AllowAny', # Just for now... 136 | ] 137 | } 138 | ... 139 | ``` 140 | 141 | ## Setting up APIs for an App 142 | 143 | 1. Create serializers for the models in the app. 144 | 145 | ```python 146 | # products/serializers.py (a new file) 147 | from rest_framework import serializers 148 | from . models import Product 149 | 150 | class ProductSerializer(serializers.ModelSerializer): 151 | 152 | class Meta: 153 | fields = {'id', 'created_by', 'name', 'description', 'image', 'created_at', } 154 | model = Product 155 | ``` 156 | 157 | 2. Set up URLs for the app. 158 | 159 | First, create paths for app (create `products/urls.py`): 160 | 161 | ```python 162 | # products/urls.py -- new file added 163 | 164 | from . views import ProductList, ProductDetail 165 | 166 | urlpatterns = [ 167 | path('', ProductList.as_view()), 168 | path('/' ProductDetail.as_view()), 169 | ] 170 | ``` 171 | 172 | Then, create path in project-level URLS file: 173 | 174 | ```python 175 | # my_project/urls.py 176 | from django.contrib.import admin 177 | from django.urls import path, include # 'include' added in this step 178 | 179 | urlpatterns = [ 180 | path('admin/', admin.site.urls), 181 | path('api/v1/products/', include('products.urls')) # Added in this step 182 | ] 183 | ``` 184 | 185 | 3. Set up the views for the app: 186 | 187 | ```python 188 | # products/views.py 189 | from rest_framework import generics # Added 190 | 191 | from . models import Product # Added 192 | from . serializers import ProductSerializer # Added 193 | 194 | class ProductList(generics.ListCreateAPIView): 195 | queryset = Product.objects.all() 196 | serializer_class = ProductSerializer 197 | 198 | class ProductDetail(generics.RetrieveUpdateDestroyAPIView): 199 | queryset = Product.objects.all() 200 | serializer_class = ProductSerializer 201 | ``` 202 | 203 | ## Allow Cross-Origin Resource Sharing (CORS) 204 | 205 | 1. Install package 206 | 207 | ``` 208 | (projdir) $ pipenv install django-cors-headers==2.4.0 209 | ``` 210 | 211 | 2. Update maindir/settings.py 212 | 2a. Add `corsheaders` in `INSTALLED_APPS`: 213 | 214 | ```python 215 | INSTALLED_APPS = [ 216 | ... 217 | # 3rd party 218 | 'rest_framework', 219 | 'corsheaders', 220 | ... 221 | ] 222 | ``` 223 | 224 | 2b. Add middlewares to the front of the `MIDDLEWARE` list: 225 | 226 | ```python 227 | MIDDLEWARE = [ 228 | 'corsheaders.middleware.CorsMiddleware', 229 | 'django.middleware.common.CommonMiddleware', 230 | ... 231 | ] 232 | ``` 233 | 234 | 2c. Add `CORS_ORIGIN_WHITELIST`: 235 | 236 | ```python 237 | CORS_ORIGIN_WHITELIST = ( 238 | 'mydomain.com' # Whatever domain 239 | ) 240 | ``` 241 | 242 | ## Setting Permissions 243 | 244 | #### View-Level Permissions 245 | 246 | 1. In the views.py file, import `permissions` from the `rest_framework` package. 247 | 248 | ```python 249 | from rest_framework import generics, permissions 250 | ``` 251 | 252 | 2. Add `permissions_classes` member variable to a view class. 253 | 254 | ```python 255 | class ProductList(generics.ListCreateAPIView): 256 | permissions_classes = (permissions.IsAuthenticated,) 257 | ``` 258 | 259 | #### Project-Level Permissions 260 | 261 | 1. Add permissions settings in setting.py, under the `REST_FRAMEWORK` object and in the `'DEFAULT_PERMISSION_CLASSES'` list. 262 | 263 | ```python 264 | # project_dir/settings.py 265 | ... 266 | REST_FRAMEWORK = { 267 | 'DEFAULT_PERMISSION_CLASSES': [ 268 | 'rest_framework.permissions.IsAuthenticated', 269 | ] 270 | } 271 | ``` 272 | 273 | Built-in project-level permissions settings include: 274 | 275 | - AllowAny 276 | - IsAuthenticated 277 | - IsAdminUser 278 | - IsAuthenticatedOrReadOnly 279 | 280 | #### Custom permissions 281 | 282 | 1. In file where custom permissions class is defined (Suggestion: make a permissions.py file), import `permissions` package: 283 | 284 | ```python 285 | from rest_framework import permissions 286 | ``` 287 | 288 | 2. Declare a class that extends `permissions.BasePermission`. Example: 289 | 290 | ```python 291 | class IsOwnerOrReadOnly(permissions.BasePermission): 292 | ``` 293 | 294 | 3. Override boolean methods `has_permission()` or `has_object_permission()` 295 | 296 | ```python 297 | def has_object_permission(self, request, view, obj): 298 | # Read-only permissions allowed for SAFE_METHODS (GET, OPTIONS, HEAD) 299 | if request.method in permissions.SAFE_METHODS: 300 | return True 301 | 302 | # Write permissions for owner 303 | return obj.owner == request.user 304 | ``` 305 | 306 | ## User Authentication 307 | 308 | #### Basic Authentication 309 | 310 | 1. Client makes HTTP request 311 | 2. Server responds with 401 status (`Unauthorized`) and `www-Authenticate` HTTP header 312 | 3. Client sends credentials back with Authorization HTTP header 313 | 4. Server checks credentials; responds with 200 OK or 403 Forbidden code. 314 | 315 | `+` Simple 316 | 317 | `-` Must send credentials for every request 318 | 319 | `-` Insecure 320 | 321 | #### Session Authentication 322 | 323 | 1. User enters credentials (logs in). 324 | 2. Server verifies credentials. 325 | 3. Server creates session object; stores in database. 326 | 4. Server sends client session ID; client stores as cookie. 327 | 5. When user logs out, session ID destroyed by client and server. 328 | 329 | `+` Credentials sent only once. 330 | 331 | `+` More efficient lookup for session ID. 332 | 333 | `-` Session ID only valid in browser where logged in. 334 | 335 | `-` Cookie sent out for every request, even no authorization required ones. 336 | 337 | #### Token Authentication 338 | 339 | 1. User sends credentials to server. 340 | 2. Unique token generated and stored by client as cookie or local storage. 341 | 3. Token passed in header of each HTTP request. 342 | 4. Server verifies token validity to see if user is authenticated. 343 | 344 | `+` Tokens stored only in client. 345 | 346 | `+` Token can be shared by multiple front-ends. 347 | 348 | `-` Tokens can become large. 349 | 350 | `-` Token usually contains all user info. 351 | 352 | ### Setting Up Token Authentication 353 | 354 | 1. In settings.py, add `'DEFAULT_AUTHENTICATION_CLASSES'` list to the `REST_FRAMEWORK` object: 355 | 356 | ```python 357 | REST_FRAMEWORK = { 358 | 'DEFAULT_PERMISSION_CLASSES': [ 359 | 'rest_framework.permissions.IsAuthenticated', 360 | ], 361 | 'DEFAULT_AUTHENTICATION_CLASSES': [ 362 | 'rest_framework.authentication.SessionAuthentication', 363 | 'rest_framework.authentication.TokenAuthentication', 364 | ] 365 | } 366 | ``` 367 | 368 | - NOTE: `SessionAuthentication` needed for using Browsable API. 369 | 370 | 2. Add `'rest_framework.authtoken'` to `INSTALLED_APPS` in settings.py: 371 | 372 | ```python 373 | INSTALLED_APPS = [ 374 | ... 375 | # 3rd party 376 | 'rest_framework', 377 | 'rest_framework.authtoken', 378 | ... 379 | ] 380 | ``` 381 | 382 | 3. Sync the database with `migrate` command. 383 | 384 | ``` 385 | (projdir) $ python manage.py migrate 386 | (projdir) $ python manage.py runserver 387 | ``` 388 | 389 | ## Setting Up Authentication APIs (using django-rest-auth) 390 | 391 | 1. Install packages via command line 392 | 393 | ``` 394 | (projdir) $ pipenv install django-rest-auth==0.9.3 395 | ``` 396 | 397 | 2. Add to `INSTALLED_APPS` in settings.py 398 | 399 | ```python 400 | INSTALLED_APPS = [ 401 | ... 402 | # 3rd party 403 | 'rest_framework', 404 | 'rest_framework.authtoken', 405 | 'rest_auth', 406 | ... 407 | ] 408 | ``` 409 | 410 | 3. Include `'rest_auth.urls'` to project's urls.py. 411 | 412 | ```python 413 | urlpatterns = [ 414 | ... 415 | path('api/v1/rest-auth/', include('rest_auth.urls')), 416 | ] 417 | ``` 418 | 419 | ## Setting Up User Registration APIs (using the django-allauth package) 420 | 421 | 1. Command line install: 422 | 423 | ``` 424 | (projdir) $ pipenv install django-allauth==0.37.1 425 | ``` 426 | 427 | 2. Add multiple configs to the `INSTALLED_APPS` list in settings.py 428 | 429 | ```python 430 | INSTALLED_APPS = [ 431 | ... 432 | 'django.contrib.sites', 433 | 434 | # 3rd party 435 | 'rest_framework', 436 | 'rest_framework.authtoken', 437 | 'allauth', ## 438 | 'allauth.account', ## 439 | 'allauth.socialaccount', ## 440 | 'rest_auth', 441 | 'rest_auth.registration', ## 442 | ... 443 | ] 444 | ... 445 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 446 | 447 | SITE_ID = 1 448 | ``` 449 | 450 | 3. Add url route in `projdir/urls.py`. 451 | 452 | ```python 453 | urlpatterns = [ 454 | ... 455 | path('api/v1/rest-auth/registration/', include('rest_auth.registration.urls')), 456 | ] 457 | ``` 458 | 459 | ## Implementing Viewsets (e.g., improving on views) 460 | 461 | 1. In an app's views.py, import `viewsets` from the `rest_framework` package. 462 | 463 | ```python 464 | from rest_framework import viewsets 465 | ``` 466 | 467 | (We are additionally no longer requiring the importing of `generics` or `permissions`.) 468 | 469 | 2. Create view class that extends `viewset.ModelViewSet`. 470 | 471 | ```python 472 | class ProductViewSet(viewset.ModelViewSet): 473 | permission_classes = (IsAuthorOrReadOnly,) 474 | queryset = Product.objects.all() 475 | serializer_class = ProductSerializer 476 | ``` 477 | 478 | ## Implementing Routers (e.g., improving of url patterns) 479 | 480 | 1. Import `SimpleRouter` from the `rest_framework.routers` package. 481 | 482 | ```python 483 | from rest_framework.routers import SimpleRouter 484 | ``` 485 | 486 | 2. Import the Viewset classes (we also no longer import the old View classes). 487 | 488 | ```python 489 | from .views import ProductViewSet 490 | ``` 491 | 492 | 3. Create an instance of `SimpleRouter`, register routes for the `ViewSet`s, and then set the `urlpatterns` variable to the router's urls. 493 | 494 | ```python 495 | router = SimpleRouter() 496 | router.register('products', ProductViewSet, base_name='products') 497 | 498 | urlpatterns = router.urls 499 | ``` 500 | 501 | ## Generating API Schema 502 | 503 | 1. Install Core API package via command line. 504 | 505 | ``` 506 | (projdir) $ pipenv install coreapi==2.3.3 507 | ``` 508 | 509 | 2. Set up `get_schema_view` from the `rest_framework.schemas` package in projdir urls.py. 510 | 511 | ```python 512 | from rest_framework.schemas import get_schema_view 513 | 514 | schema_view = get_schema_view(title='My APIs') 515 | ``` 516 | 517 | 3. Set up route for the schema. 518 | 519 | ```python 520 | urlpatterns = [ 521 | ... 522 | path('schema/', schema_view), 523 | ] 524 | ``` 525 | 526 | ## Implementing API Documentation 527 | 528 | #### Option 1: Using Django REST Framework's Built-In Documentation 529 | 530 | 1. In projdir urls.py, import `include_docs_urls` from `rest_framework`. 531 | 532 | ```python 533 | from rest_framework.documentation import include_docs_urls 534 | ``` 535 | 536 | 2. Add the route for the documentation. 537 | 538 | ```python 539 | urlpatterns = [ 540 | ... 541 | path('docs/', include_docs_urls(title='My APIs')), 542 | path('schema/', schema_view), 543 | ] 544 | ``` 545 | 546 | #### Option 2: Using Django REST Swagger 547 | 548 | 1. Install `django-rest-swagger` package. 549 | 550 | ``` 551 | (projdir) $ pipenv install django-rest-swagger==2.2.0 552 | ``` 553 | 554 | 2. Add app to the `INSTALLED_APPS` list in settings.py. 555 | 556 | ```python 557 | INSTALLED_APPS = [ 558 | ... 559 | # 3rd party 560 | ... 561 | 'rest_framework.authtoken', 562 | 'rest_framework_swagger', 563 | 'allauth', 564 | ... 565 | ] 566 | ``` 567 | 568 | 3. In projdir urls.py, import `get_swagger_view`, and use it when creating the `'swagger-docs'` route. 569 | 570 | ```python 571 | from rest_framework_swagger.views import get_swagger_view 572 | ... 573 | API_TITLE = 'My APIs' 574 | schema_view = get_swagger_view(title=API_TITLE) 575 | ... 576 | urlpatterns = [ 577 | ... 578 | path('swagger-docs/', schema_view), 579 | ] 580 | ``` 581 | 582 | 4. To tie the login/logout buttons in the swagger view to the APIs for login/logout in settings.py. 583 | 584 | ```python 585 | SWAGGER_SETTINGS = { 586 | 'LOGIN_URL': 'rest_framework:login', 587 | 'LOGOUT_URL': 'rest_framework:logout', 588 | } 589 | ``` 590 | 591 | ## Adding Pagination 592 | 593 | 1. In settings.py, add two properties: `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` 594 | 595 | ```python 596 | REST_FRAMEWORK = { 597 | ... 598 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 599 | 'PAGE_SIZE': 10 600 | } 601 | ``` 602 | --------------------------------------------------------------------------------