This project was inspired by Dicoding Final Project Backend API. Initially I was finished the scholarship Dicoding Indonesia class of "Belajar Membuat Aplikasi Backend untuk Pemula". I got 5 stars for my final grade but it quickly came to an end and I was not satisfied with the end results.
REST API with CRUD operations on books objects. Users can send requests to API to view books anonymously. In order to create/update/delete books JWT is required. (Authorization: Bearer {token} attached to the headers). API provides endpoints for registration, token retrieval, change the password, update profile, and unauthorization token. I used black list as a log out functionality. The simple JWT blacklist app implements its outstanding and blacklisted token lists using two models: OutstandingToken and BlacklistedToken.
- Django + Django REST Framework
Json Web Token Authentication is 'rest_framework_simplejwt' package, which provides JWT implementation including DRF views for obtaining and refreshing tokens.
- Visual Studio Code, Postman
Production database is Heroku Postgres. Postgres is also used by Github Actions where unit tests are ran. Locally it used default SQLite database.
ERD diagram including Django's base entities and manually created Book entity.
Book has its own name, author, publisher, etc. It stores two datetime attributes: insertedAt and updatedAt which both are initially set on record's creation. Books are saved and updated using modified ModelSerializer.
HTTP Method | API endpoint | Request body | Response body | Description | Authorization header |
---|---|---|---|---|---|
POST | api/auth/register/ | Object { username: str, password: str, password2: str, email: str, first_name: str, last_name: str } |
Object { id: number, username: str, email: str, first_name: str, last_name: str } |
Creates new user. Returns simplified user object. | None |
POST | api/auth/login/ | Object { username: str, password: str } |
Object { refresh: str, access: str } |
Returns personal JWT access and refresh tokens. | None |
POST | api/auth/login/refresh/ | Object { refresh: str } |
Object { access: str } |
Returns refreshed JWT access token. | None |
PUT | api/auth/password/{id}/ | Object { password: str, password2: str, old_password: str } |
X | Performs change on password with given ID. Returns updated password. | Bearer {token} |
PUT | api/auth/profile/{id}/ | Object { username: str, first_name: str, last_name: str, email: str } |
X | Performs change on profile with given ID. Returns updated object. | Bearer {token} |
POST | api/auth/logout/ | Object { refresh: str } |
X | Perform unauthorization user (logout). | Bearer {token} |
GET | api/books/ | X | Array<Object> [ Object { id: str, name: str, published: str } ] |
Lists all of the existing book objects. | None |
POST | api/books/ | Object { name: str, year: number, author: str, summary: str, publisher: str, pageCount: number, readPage: number, } |
Object { bookId: str } |
Creates and returns new book object with given content. | Bearer {token} |
GET | api/books/{id}/ | X | Object { id: str, name: str, year: number, author: str, summary: str, publisher: str, pageCount: number, readPage: number, finished: boolean, reading: boolean, insertedAt: datetime, updatedAt: datetime } |
Retrieves book object with given ID. | None |
PUT | api/books/{id}/ | Object { name: str, year: number, author: str, summary: str, publisher: str, pageCount: number, readPage: number, } |
X | Perfoms full update on book object with given ID. Returns updated object. | Bearer {token} |
PATCH | api/books/{id}/ | Object { content: str } |
X | Performs partial or full update on book object with given ID. Returns updated object. | Bearer {token} |
DELETE | api/books/{id}/ | X | X | Deletes book object with given ID. | Bearer {token} |
Application also uses Swagger for documentation purposes and also as a simpler and more visually appealing interface than individual REST Framework views. You can see its main view here.
Also you can see Schema for more detail API documentation. You can see its main view here
There are 2 urls which need to be handled in REST type API.
One for listing objects and creating new ones and one for operating on a specific object such as list, update, and delete by id.
Since in REST architecture there should no be endpoints such as api/books/update/{id}
, api/books/delete{id}
or anything like that, there is no need for creating a view for each CRUD operation.
ListCreateBook
handles api/books
endpoint. Allows GET, POST, and safe methods HEAD, OPTIONS.
ListUpdateDeleteBookById
handles api/books/{id}
endpoint. ALlows GET, PUT, DELETE, and safe methods HEAD, OPTIONS.
Here are some examples how you can interact with API using different tools (curl, Python).
Recommended using tool Postman.
Register yourself if you do not own an account.
curl -d "username=YourUserName&password=YourPassword" -X POST https://books-api-dicoding.herokuapp.com/api/auth/register/
Get your JWT token.
const request = fetch("http://127.0.0.1:8000/api/auth/token/", {
method: "POST",
headers: {
"Content-Type": "Application/json",
},
body: JSON.stringify({
username: "YourUserName",
password: "YourPassword",
}),
})
.then((res) => res.json())
.then((data) => console.log(data));
const { refresh, access } = request;
If your token expires (access token lives for 1 hours, refresh token - 24h)
const refresh = "refresh token you previously redeemed or had stored";
const request = fetch("http://127.0.0.1:8000/api/auth/token/refresh/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
refresh: refresh,
}),
})
.then((res) => res.json())
.then((data) => console.log(data));
const { access } = request;
Now you can send requests to the API endpoints. Attach Authorization header if you want to POST/PUT/DELETE.
import json
import requests
token = "your JWT access token"
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
# get all books - no token needed
books = requests.get('http://127.0.0.1:8000/api/books/')
print(books.json())
# create new book
payload = json.dumps({
'name': 'book-name',
'year': 2021,
'author': 'author-name',
'summary': 'book-summary',
'publisher': 'book-publisher',
'pageCount': 400,
'readPage': 0
})
new_book = requests.post('http://127.0.0.1:8000/api/books/',
data=payload,
headers=headers
)
new_book = new_book.json()
# get book - no token needed
bookid = new_book['data']['bookId']
book = requests.get(f'http://127.0.0.1:8000/api/books/{bookid}')
print(book.json())
# update book
payload = json.dumps({
'name': 'book-name-updated',
'year': 2022,
'author': 'author-name-updated',
'summary': 'book-summary-updated',
'publisher': 'book-publisher-updated',
'pageCount': 400,
'readPage': 399
})
updated_book = requests.put(f'http://127.0.0.1:8000/api/books/{bookid}',
data=payload,
headers=headers
)
print(updated_book.json())
# delete book
delete_book = requests.delete(f'http://127.0.0.1:8000/api/books/{bookid}',
headers=headers
)
print(delete_book.json())
All of API endpoints have their own unit tests. This repository has its own Github workflows testing pipeline.
- Get all books (empty list or few objects)
- Unauthorized POST request
- Not allowed method
- Create book: with valid content, invalid content such as: without name, readPage > pageCount.
- Get all books with specified query endpoint, currently only reading and finished Query endpoint.
- Unaothorized PUT/DELETE
- Not allowd method
- Get/Update/Delete book: with valid content, invalid content such as: without name, readPage > pageCount, there's no id.
- Register new user, get token for them, refresh token (views from external packages are already tested by the authors)
- Update profile, change password, unauthorize user (logout), blacklist token and outstanding token.
This repository has been deployed to Heroku. You can visit here
- Create staticfiles folder and put any file into it. (Make sure you made an exception in .gitignore for this file. Mine is called temp.)
- Make sure there is Procfile is root directory with these 2 lines:
release: python manage.py migrate --no-input
web: gunicorn core.wsgi
- Set
DEBUG = False
, adddjango_heroku.settings(locals())
on the bottom of settings.py. Make sure your requirements.txt contains every needed package. You may want to update it withpip freeze > requirements.txt
. - Go to Heroku and add new app.
- Go to Resources tab and install Heroku Postgres add-on.
- Go to Settings tab and set SECRET_KEY in config vars. Add heroku/python buildpack.
- Go to Deploy tab, connect your Github repository, select branch to deploy. You can Enable Automatic Deploys or Deploy Branch manually.
- App should be up and running at
https://<name_of_app>.herokuapp.com
.
Create new virtual environment, activate it and install dependencies.
pip3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
Set SECRET_KEY in your environmental variables.
You can also install python-dotenv
, put .env
file with secrets in root directory
and add those lines in settings.py. (Make sure .env is not being commited to git repository if you want to use this method)
from dotenv import load_dotenv
load_dotenv()
Run migrations and create super user. (Making migrations might not be necessary)
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
Run server and open your browser at http://127.0.0.1:8000/
.
python manage.py runserver
Run tests with coverage (unit tests + report)
coverage run --omit='*/venv/*' manage.py test
coverage report -m
You can make report from htmlcoverage by default
coverage run --omit='*/venv/*' manage.py test
coverage html
Find index.html in htmlcov folder and open it to browser to see the tests report
- Permissions to access book (e.g only author can modify)
- More Query endpoint(get book by other attributes)
- Pagination (so that user would not receive everything at once)