Рассмотренного в прошлых темах материала достаточно для создания примитивного приложения. В этой теме попробуем реализовать простейшее приложение Web API в стиле REST. Архитектура REST предполагает применение следующих методов или типов запросов HTTP для взаимодействия с сервером, где каждый тип запроса отвечает за определенное действие:
GET (получение данных)
POST (добавление данных)
PUT (изменение данных)
DELETE (удаление данных)
Для каждого из этих типов запросов класс FastAPI предоставляет соответствующие методы. Рассмотрим, как мы можем реализовать с помощью этих методов простейший API. Пусть у нас будет следующий проект:
В файле main.py определим следующий код:
import uuid
from fastapi import FastAPI, Body, status
from fastapi.responses import JSONResponse, FileResponse
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
self.id = str(uuid.uuid4())
# условная база данных - набор объектов Person
people = [Person("Tom", 38), Person("Bob", 42), Person("Sam", 28)]
# для поиска пользователя в списке people
def find_person(id):
for person in people:
if person.id == id:
return person
return None
app = FastAPI()
@app.get("/")
async def main():
return FileResponse("public/index.html")
@app.get("/api/users")
def get_people():
return people
@app.get("/api/users/{id}")
def get_person(id):
# получаем пользователя по id
person = find_person(id)
print(person)
# если не найден, отправляем статусный код и сообщение об ошибке
if person==None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
#если пользователь найден, отправляем его
return person
@app.post("/api/users")
def create_person(data = Body()):
person = Person(data["name"], data["age"])
# добавляем объект в список people
people.append(person)
return person
@app.put("/api/users")
def edit_person(data = Body()):
# получаем пользователя по id
person = find_person(data["id"])
# если не найден, отправляем статусный код и сообщение об ошибке
if person == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
# если пользователь найден, изменяем его данные и отправляем обратно клиенту
person.age = data["age"]
person.name = data["name"]
return person
@app.delete("/api/users/{id}")
def delete_person(id):
# получаем пользователя по id
person = find_person(id)
# если не найден, отправляем статусный код и сообщение об ошибке
if person == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
# если пользователь найден, удаляем его
people.remove(person)
return person
Разберем в общих чертах этот код. Прежде всего для представления данных, с которыми мы будем работать, определяем класс Person.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
self.id = str(uuid.uuid4())
Этот
класс содержит три атрибута. Два атрибута - name и age будут представлять имя и возраст пользователя и будут устанавливаться через конструктор. А третий атрибут - id
будет служить для уникальной идентификации данного объекта и будет хранить значение guid. Для генерации guid применяется функция uuid.uuid4()
из пакета uuid. В конструкторе Person сгенерированный guid преобразуется в строку и присваивается атрибуту id.
Для хранения данных в приложении определим список people, который будет выполнять роль условной базы данных и будет хранить объекты Person.
people = [Person("Tom", 38), Person("Bob", 42), Person("Sam", 28)]
Для поиска объекта Person в этом списке определена вспомогательная функция find_person().
При обращении к корню веб-приложения, то есть по пути "/", оно будет отправлять в ответ файл index.html, то есть веб-страницу, посредством которой
пользователь сможет взаимодействовать с сервером:
@app.get("/")
def main():
return FileResponse("public/index.html")
Далее определяются функции, которые собственно и представляют API. Вначале определяется функция, которая обрабатывает запрос типа GET по пути "api/users":
@app.get("/api/users")
def get_people():
return people
Запрос GET предполагает получение объектов, и в данном случае отправляем выше определенный список объектов Person.
Когда клиент обращается к приложению для получения одного объекта по id в запрос типа GET по адресу "api/users/{id}", то срабатывает другая функция:
@app.get("/api/users/{id}")
def get_person(id):
person = find_person(id)
if person==None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
return person
Здесь через параметр id получаем из пути запроса идентификатор объекта Person и по этому идентификатору ищем нужный объект в списке people.
Если объект по id не был найден, то возвращаем с помощью класса JSONResponse статусный код 404 с некоторым сообщением в формате JSON.
Если объект найден, то отправляем найденный объект клиенту.
При получении запроса типа DELETE по маршруту "/api/users/{id}" срабатывает другая функция:
@app.delete("/api/users/{id}")
def delete_person(id):
person = find_person(id)
if person == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
people.remove(person)
return person
Здесь действует аналогичная логика - если объект по id не найден, отправляет статусный код 404. Если же объект найден, то удаляем его из списка и посылаем клиенту.
При получении запроса с методом POST по адресу "/api/users" срабатывает следующая функция:
@app.post("/api/users")
def create_person(data = Body()):
person = Person(data["name"], data["age"])
people.append(person)
return person
Запрос типа POST предполагает передачу приложению отправляемых данных. Причем мы ожидаем, что клиент отправит данные, которые содержат значения name и age. Для краткости мы пока опускаем валидацию входных данных. И для получения данных из тела запроса с помощью класса Body получаем данные в параметр data и затем используем данные из этого параметра для создания объекта Person. Затем созданный объект добавляется в список people и отправляется назад клиенту.
Если приложению приходит PUT-запрос по адресу "/api/users", то аналогичным образом получаем отправленные клиентом данные в виде объекта Person и пытаемся найти подобный объект в списке people. Если объект не найден, отправляем статусный код 404. Если объект найден, то изменяем его данные и отправляем обратно клиенту:
@app.put("/api/users")
def edit_person(data = Body()):
person = find_person(data["id"])
if person == None:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={ "message": "Пользователь не найден" }
)
person.age = data["age"]
person.name = data["name"]
return person
Таким образом, мы определили простейший API. Теперь добавим код клиента.
Теперь в проекте определим папку public, в которую добавим новый файл index.html
Определим в файле index.html следующим код для взаимодействия с сервером FastAPI:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>METANIT.COM</title>
<style>
td {padding:5px;}
button{margin: 5px;}
</style>
</head>
<body>
<h2>Список пользователей</h2>
<div>
<input type="hidden" id="userId" />
<p>
Имя:<br/>
<input id="userName" />
</p>
<p>
Возраст:<br />
<input id="userAge" type="number" />
</p>
<p>
<button id="saveBtn">Сохранить</button>
<button id="resetBtn">Сбросить</button>
</p>
</div>
<table>
<thead><tr><th>Имя</th><th>Возраст</th><th></th></tr></thead>
<tbody>
</tbody>
</table>
<script>
// Получение всех пользователей
async function getUsers() {
// отправляет запрос и получаем ответ
const response = await fetch("/api/users", {
method: "GET",
headers: { "Accept": "application/json" }
});
// если запрос прошел нормально
if (response.ok === true) {
// получаем данные
const users = await response.json();
const rows = document.querySelector("tbody");
// добавляем полученные элементы в таблицу
users.forEach(user => rows.append(row(user)));
}
}
// Получение одного пользователя
async function getUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "GET",
headers: { "Accept": "application/json" }
});
if (response.ok === true) {
const user = await response.json();
document.getElementById("userId").value = user.id;
document.getElementById("userName").value = user.name;
document.getElementById("userAge").value = user.age;
}
else {
// если произошла ошибка, получаем сообщение об ошибке
const error = await response.json();
console.log(error.message); // и выводим его на консоль
}
}
// Добавление пользователя
async function createUser(userName, userAge) {
const response = await fetch("api/users", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
name: userName,
age: parseInt(userAge, 10)
})
});
if (response.ok === true) {
const user = await response.json();
document.querySelector("tbody").append(row(user));
}
else {
const error = await response.json();
console.log(error.message);
}
}
// Изменение пользователя
async function editUser(userId, userName, userAge) {
const response = await fetch("api/users", {
method: "PUT",
headers: { "Accept": "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
id: userId,
name: userName,
age: parseInt(userAge, 10)
})
});
if (response.ok === true) {
const user = await response.json();
document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user));
}
else {
const error = await response.json();
console.log(error.message);
}
}
// Удаление пользователя
async function deleteUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "DELETE",
headers: { "Accept": "application/json" }
});
if (response.ok === true) {
const user = await response.json();
document.querySelector(`tr[data-rowid='${user.id}']`).remove();
}
else {
const error = await response.json();
console.log(error.message);
}
}
// сброс данных формы после отправки
function reset() {
document.getElementById("userId").value =
document.getElementById("userName").value =
document.getElementById("userAge").value = "";
}
// создание строки для таблицы
function row(user) {
const tr = document.createElement("tr");
tr.setAttribute("data-rowid", user.id);
const nameTd = document.createElement("td");
nameTd.append(user.name);
tr.append(nameTd);
const ageTd = document.createElement("td");
ageTd.append(user.age);
tr.append(ageTd);
const linksTd = document.createElement("td");
const editLink = document.createElement("button");
editLink.append("Изменить");
editLink.addEventListener("click", async() => await getUser(user.id));
linksTd.append(editLink);
const removeLink = document.createElement("button");
removeLink.append("Удалить");
removeLink.addEventListener("click", async () => await deleteUser(user.id));
linksTd.append(removeLink);
tr.appendChild(linksTd);
return tr;
}
// сброс значений формы
document.getElementById("resetBtn").addEventListener("click", () => reset());
// отправка формы
document.getElementById("saveBtn").addEventListener("click", async () => {
const id = document.getElementById("userId").value;
const name = document.getElementById("userName").value;
const age = document.getElementById("userAge").value;
if (id === "")
await createUser(name, age);
else
await editUser(id, name, age);
reset();
});
// загрузка пользователей
getUsers();
</script>
</body>
</html>
Основная логика здесь заключена в коде javascript. При загрузке страницы в браузере получаем все объекты из БД с помощью функции getUsers():
async function getUsers() {
// отправляет запрос и получаем ответ
const response = await fetch("/api/users", {
method: "GET",
headers: { "Accept": "application/json" }
});
// если запрос прошел нормально
if (response.ok === true) {
// получаем данные
const users = await response.json();
const rows = document.querySelector("tbody");
// добавляем полученные элементы в таблицу
users.forEach(user => rows.append(row(user)));
}
}
Для добавления строк в таблицу используется функция row(), которая возвращает строку. В этой строке будут определены ссылки для изменения и удаления пользователя.
Ссылка для изменения пользователя с помощью функции getUser() получает с сервера выделенного пользователя:
async function getUser(id) {
const response = await fetch(`/api/users/${id}`, {
method: "GET",
headers: { "Accept": "application/json" }
});
if (response.ok === true) {
const user = await response.json();
document.getElementById("userId").value = user.id;
document.getElementById("userName").value = user.name;
document.getElementById("userAge").value = user.age;
}
else {
// если произошла ошибка, получаем сообщение об ошибке
const error = await response.json();
console.log(error.message); // и выводим его на консоль
}
}
И выделенный пользователь добавляется в форму над таблицей. Эта же форма применяется и для добавления объекта. С помощью скрытого поля, которое хранит id пользователя, мы можем узнать, какое действие выполняется - добавление или редактирование. Если id не установлен (равен пустой строке), то выполняется функция createUser, которая отправляет данные в POST-запросе:
async function createUser(userName, userAge) {
const response = await fetch("api/users", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
name: userName,
age: parseInt(userAge, 10)
})
});
if (response.ok === true) {
const user = await response.json();
document.querySelector("tbody").append(row(user));
}
else {
const error = await response.json();
console.log(error.message);
}
}
Если же ранее пользователь был загружен на форму, и в скрытом поле сохранился его id, то выполняется функция editUser, которая отправляет PUT-запрос:
async function editUser(userId, userName, userAge) {
const response = await fetch("api/users", {
method: "PUT",
headers: { "Accept": "application/json", "Content-Type": "application/json" },
body: JSON.stringify({
id: userId,
name: userName,
age: parseInt(userAge, 10)
})
});
if (response.ok === true) {
const user = await response.json();
document.querySelector(`tr[data-rowid='${user.id}']`).replaceWith(row(user));
}
else {
const error = await response.json();
console.log(error.message);
}
}
И функция deleteUser() посылает на сервер запрос типа DELETE на удаление пользователя, и при успешном удалении на сервере
удаляет объект по id из списка объектов Person.
Теперь запустим проект, и по умолчанию приложение отправит браузеру веб-страницу index.html, которая загрузит список объектов:
После этого мы сможем выполнять все базовые операции с пользователями - получение, добавление, изменение, удаление. Например, добавим нового пользователя: