Skip to content

Recedivies/BooksAPI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

40 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BooksAPI

Python Django Heroku

Motivation behind this project and features:

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.

Description:

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.

Used Frameworks, libraries, tools:

  • 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.

Tools:

  • 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.

Database schema:

ERD diagram including Django's base entities and manually created Book entity. Entity Relationship Diagram

Book Model:

Entity Relationship Diagram

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.

API Endpoints:

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

Views:

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.

How to use this API:

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())

Testing:

All of API endpoints have their own unit tests. This repository has its own Github workflows testing pipeline.

api/books tests:
  • 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.
api/books/{id} tests:
  • 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.
api/auth/... tests:
  • 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.

Deployment:

This repository has been deployed to Heroku. You can visit here

Step to reproduce deployment:

  1. Create staticfiles folder and put any file into it. (Make sure you made an exception in .gitignore for this file. Mine is called temp.)
  2. Make sure there is Procfile is root directory with these 2 lines:
    release: python manage.py migrate --no-input
    web: gunicorn core.wsgi
  3. Set DEBUG = False, add django_heroku.settings(locals()) on the bottom of settings.py. Make sure your requirements.txt contains every needed package. You may want to update it with pip freeze > requirements.txt.
  4. Go to Heroku and add new app.
  5. Go to Resources tab and install Heroku Postgres add-on.
  6. Go to Settings tab and set SECRET_KEY in config vars. Add heroku/python buildpack.
  7. Go to Deploy tab, connect your Github repository, select branch to deploy. You can Enable Automatic Deploys or Deploy Branch manually.
  8. App should be up and running at https://<name_of_app>.herokuapp.com.

Local development:

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

Possible future content:

  • 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)