Creating an API with python: Part 3: POST Endpoints

In my previous post, Creating an API with python: Part 2: MariaDB Database, I set up a MariaDB database and connected it to the five GET endpoints in the FastAPI API. In this post, I’ll add three POST endpoints which will add data to the database.

Prerequisites

  1. Creating an API with python: Part 1: GET Endpoints
  2. Creating an API with python: Part 2: MariaDB Database

Step 1: Add schemas.py

Create a new python file called schemas.py. This will contain the pydantic schemas that will be used to validate the POST data for the POST endpoints. They will also provide descriptions for the swagger docs page.

  1. Change to the code directory (~/vboxshare/fastapi should be replaced with the path to your FastAPI python code):
    $ cd ~/vboxshare/fastapi
    
  2. Change to the manager directory.
    $ cd manager
    
  3. Create a file called schemas.py and add the following to it:
    from typing import Optional
    
    from pydantic import BaseModel, Field
    
    
    class PostLink(BaseModel):
        link: str = Field(..., description="The link URL")
        tag: Optional[str] = Field(None,
                                   description="Tag name to associate with the link (will be created if it doesn't exist)")
        tag_id: Optional[str] = Field(None, description="Tag ID to associate with the link (must already exist)")
    
    
    class PostTag(BaseModel):
        tag: str = Field(..., description="Tag name")
    
    
    class PostTagLink(BaseModel):
        tag_id: str = Field(..., description="Tag ID (must already exist)")
        link_id: str = Field(..., description="Link ID (must already exist)")
    

Step 2: Update manager.py

Update the manager/manager.py file with new functions for creating a link, tag and taglink (create_link, create_tag, create_taglink).

  1. Open the manager/manager.py file and replace the contents with the following code:
    from typing import Optional
    
    from uuid import uuid4
    
    from sqlalchemy.orm import Session
    
    from fastapi import HTTPException
    
    from manager import models, schemas
    
    
    def get_link(db: Session, link_id: str):
        return db.query(models.Link).filter(models.Link.link_id == link_id).first()
    
    
    def get_tag(db: Session, tag_id: str):
        return db.query(models.Tag).filter(models.Tag.tag_id == tag_id).first()
    
    
    def get_tag_by_tag_name(db: Session, tag: str):
        return db.query(models.Tag).filter(models.Tag.tag == tag).first()
    
    
    def get_links(db: Session, tag_id: Optional[str] = None, tag: Optional[str] = None):
        if tag is None and tag_id is None:
            # TODO: Implement offset and limit
            return db.query(models.Link).all()
        elif tag is not None:
            tag_record = get_tag_by_tag_name(db, tag)
            if tag_record is None:
                return []
            tag_id = tag_record.tag_id
    
        return db.query(models.Link).join(models.TagLink).filter(models.TagLink.tag_id == tag_id).all()
    
    
    def get_tags(db: Session, tag: Optional[str] = None):
        if tag is None:
            # TODO: Implement offset and limit
            return db.query(models.Tag).all()
        else:
            db_tag = get_tag_by_tag_name(db, tag)
            if db_tag is not None:
                return [db_tag]
            return []
    
    
    def get_taglinks(db: Session, tag_id: Optional[str] = None, link_id: Optional[str] = None):
        if tag_id is None and link_id is None:
            # TODO: Implement offset and limit
            return db.query(models.TagLink).all()
        elif tag_id is not None and link_id is None:
            return db.query(models.TagLink).filter(models.TagLink.tag_id == tag_id).all()
        elif tag_id is None and link_id is not None:
            return db.query(models.TagLink).filter(models.TagLink.link_id == link_id).all()
        else:
            return db.query(models.TagLink).filter(models.TagLink.link_id == link_id, models.TagLink.tag_id == tag_id).all()
    
    
    def create_link(db: Session, link: schemas.PostLink):
        db_link = models.Link(link_id=str(uuid4()), link=link.link)
        db.add(db_link)
        tag_id = link.tag_id
        link_id = db_link.link_id
    
        if link.tag is not None:
            db_tag = get_tag_by_tag_name(db, link.tag)
            if db_tag is None:
                db_tag = models.Tag(tag_id=str(uuid4()), tag=link.tag)
                db.add(db_tag)
                tag_id = db_tag.tag_id
            else:
                tag_id = db_tag.tag_id
        elif tag_id is not None:
            db_tag = get_tag(db, tag_id)
            if db_tag is None:
                raise HTTPException(status_code=404, detail=f"Tag with tag_id {tag_id} not found")
    
        db_taglink = models.TagLink(link_id=link_id, tag_id=tag_id)
        db.add(db_taglink)
    
        db.commit()
        db.refresh(db_link)
        return db_link
    
    
    def create_tag(db: Session, tag: schemas.PostTag):
        db_tag = models.Tag(tag_id=str(uuid4()), tag=tag.tag)
        db_tag_existing = get_tags(db, tag=tag.tag)
        if len(db_tag_existing) > 0:
            raise HTTPException(status_code=409, detail=f"Tag with name {tag.tag} exists")
        db.add(db_tag)
        db.commit()
        db.refresh(db_tag)
        return db_tag
    
    
    def create_taglink(db: Session, taglink: schemas.PostTagLink):
        tag_id = taglink.tag_id
        link_id = taglink.link_id
        db_taglink = models.TagLink(tag_id=tag_id, link_id=link_id)
        db_tag_id_existing = get_tag(db, tag_id)
        if db_tag_id_existing is None:
            raise HTTPException(status_code=422, detail=f"Tag with tag_id {tag_id} not found")
        db_link_id_existing = get_link(db, link_id)
        if db_link_id_existing is None:
            raise HTTPException(status_code=422, detail=f"Link with link_id {link_id} not found")
        db_taglink_existing = get_taglinks(db, tag_id=tag_id, link_id=link_id)
        if len(db_taglink_existing) > 0:
            raise HTTPException(status_code=409, detail=f"TagLink with tag_id {tag_id} and link_id {link_id} exists")
        db.add(db_taglink)
        db.commit()
        db.refresh(db_taglink)
        return db_taglink
    

Step 3: Update main.py

Update main.py with a rollback in the event of a database exception, and add functions post_link, post_tag and post_taglink as the three new POST endpoint functions.

  1. Change directory to the top level directory:
    $ cd ../
    
  2. Replace the contents of main.py with:
    from typing import Optional
    
    from sqlalchemy.orm import Session
    
    from fastapi import FastAPI, HTTPException, Depends
    
    from manager import manager, schemas
    
    from manager.database import SessionLocal
    
    
    # Dependency
    def get_db():
        db = SessionLocal()
        try:
            yield db
        except Exception:
            db.rollback()
        finally:
            db.close()
    
    
    app = FastAPI()
    
    
    # Get link by link_id
    @app.get("/link/{link_id}")
    async def get_link(link_id: str, db: Session = Depends(get_db)):
        db_link = manager.get_link(db, link_id)
        if db_link is None:
            raise HTTPException(status_code=404, detail="Link not found")
        return db_link
    
    
    # Get links by query params
    @app.get("/link/")
    async def get_links(tag_id: Optional[str] = None, tag: Optional[str] = None, db: Session = Depends(get_db)):
        return manager.get_links(db, tag_id, tag)
    
    
    # Get tag by tag_id
    @app.get("/tag/{tag_id}")
    async def get_tag(tag_id: str, db: Session = Depends(get_db)):
        db_tag = manager.get_tag(db, tag_id)
        if db_tag is None:
            raise HTTPException(status_code=404, detail="Tag not found")
        return db_tag
    
    
    # Get tags by query params
    @app.get("/tag/")
    async def get_tags(tag: Optional[str] = None, db: Session = Depends(get_db)):
        return manager.get_tags(db, tag)
    
    
    # Get taglinks by query params
    @app.get("/taglink/")
    async def get_taglinks(link_id: Optional[str] = None, tag_id: Optional[str] = None,  db: Session = Depends(get_db)):
        return manager.get_taglinks(db, tag_id, link_id)
    
    
    # Post a link
    @app.post("/link/")
    async def post_link(link: schemas.PostLink, db: Session = Depends(get_db)):
        if link.tag is None and link.tag_id is None:
            raise HTTPException(status_code=422, detail="One of tag_id or tag must be specified")
    
        if link.tag is not None and link.tag_id is not None:
            raise HTTPException(status_code=422, detail="Only one of tag_id or tag must be specified")
    
        db_link = manager.create_link(db, link)
    
        return db_link
    
    
    # Post a tag
    @app.post("/tag/")
    async def post_tag(tag: schemas.PostTag, db: Session = Depends(get_db)):
        db_tag = manager.create_tag(db, tag)
    
        return db_tag
    
    
    # Post a taglink
    @app.post("/taglink/")
    async def post_taglink(taglink: schemas.PostTagLink, db: Session = Depends(get_db)):
        db_tag = manager.create_taglink(db, taglink)
    
        return db_tag
    

Step 4: Update test script

Now we need to update the test script to test the POST endpoints, retrieve the responses and test the GET endpoints with the data retrieved.

  1. Open the file test.sh, and replace the code with the following:
    IP=$1
    BASEURL=http://$IP:8000
    
    echo "=========================="
    echo "Test POST /link link=https://www.test1.com, tag=test1"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.test1.com\", \"tag\": \"test1\"}" $BASEURL/link/)
    echo $resp
    link_id1=$(echo $resp | jq -r '.link_id')
    echo $link_id1
    
    echo "=========================="
    echo "Test GET /tag tag=test1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag/?tag=test1)
    echo $resp
    tag_id1=$(echo $resp | jq -r '.[0].tag_id')
    echo $tag_id1
    
    echo "=========================="
    echo "Test POST /tag tag=test2"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"tag\": \"test2\"}" $BASEURL/tag/)
    echo $resp
    tag_id2=$(echo $resp | jq -r '.tag_id')
    echo $tag_id2
    
    echo "=========================="
    echo "Test POST /link link=https://www.test2.com, tag=test2"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.test2.com\", \"tag\": \"test2\"}" $BASEURL/link/)
    echo $resp
    link_id2=$(echo $resp | jq -r '.link_id')
    echo $link_id2
    
    echo "=========================="
    echo "Test POST /link link=https://www.test2.com, tag_id=$tag_id1"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.test2.com\", \"tag_id\": \"$tag_id1\"}" $BASEURL/link/)
    echo $resp
    link_id2=$(echo $resp | jq -r '.link_id')
    echo $link_id2
    
    echo "=========================="
    echo "Test POST /link link=https://www.test2.com, tag_id=invalid. Expect 404 response: Tag with tag_id invalid not found"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link\": \"https://www.test2.com\", \"tag_id\": \"invalid\"}" $BASEURL/link/)
    echo $resp
    
    echo "=========================="
    echo "Test GET /link"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link/)
    echo $resp
    
    echo "=========================="
    echo "Test GET /link tag_id=$tag_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link/?tag_id=$tag_id1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /link tag=test1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link/?tag=test1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /link link_id=$link_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/link/$link_id1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /tag tag_id=$tag_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag/$tag_id1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /tag"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/tag/)
    echo $resp
    
    echo "=========================="
    echo "Test POST /taglink tag_id=$tag_id2, link_id=$link_id1"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"$tag_id2\"}" $BASEURL/taglink/)
    echo $resp
    
    echo "=========================="
    echo "Test POST /taglink tag_id=invalid, link_id=$link_id1. Expect 422 response: Tag with tag_id invalid not found"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"invalid\"}" $BASEURL/taglink/)
    echo $resp
    
    echo "=========================="
    echo "Test POST /taglink tag_id=$tag_id1, link_id=invalid. Expect 422 response: Link with link_id invalid not found"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"invalid\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink/)
    echo $resp
    
    echo "=========================="
    echo "Test POST /taglink tag_id=$tag_id1, link_id=$link_id1. Expect 409 response: TagLink with tag_id $tag_id1 and link_id $link_id1 exists"
    resp=$(curl -X "POST" -H "Content-Type: application/json" -d "{\"link_id\": \"$link_id1\", \"tag_id\": \"$tag_id1\"}" $BASEURL/taglink/)
    echo $resp
    
    echo "=========================="
    echo "Test GET /taglink"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/taglink/)
    echo $resp
    
    echo "=========================="
    echo "Test GET /taglink link_id=$link_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/taglink/?link_id=$link_id1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /taglink tag_id=$tag_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/taglink/?tag_id=$tag_id1)
    echo $resp
    
    echo "=========================="
    echo "Test GET /taglink tag_id=$tag_id1 link_id=$link_id1"
    resp=$(curl -X "GET" -H "Content-Type: application/json" $BASEURL/taglink/?tag_id=$tag_id1&link_id=$link_id1)
    echo $resp
    
  2. Make sure the test script is executable:
    $ chmod ugo+x test.sh
    

Step 5: Run the test script

  1. Run the test script, replacing YOUR_IP with the IP of your API host server (or 127.0.0.1 if running the test script from the machine that is hosting the API):
    $ ./test.sh YOUR_IP
    
    If all goes well, you should see an output of json responses to each request, with no errors, other than those expected in the test descriptions.
  2. If you want to run the test script again, you will need to clear the test data from the database first. To do this, open a mysql terminal:
    $ mysql -u root -p
    
    Enter the password you set with the initial set-up of MariaDB in Part 2.
  3. Switch to the apiservice database:
    MariaDB [(none)]> USE apiservice;
    
  4. Run the following to clear the test data:
    MariaDB [(apiservice)]> DELETE from taglink where link_id IN(SELECT link_id from link where link LIKE '%test%'); DELETE from taglink where tag_id IN(SELECT tag_id from tag where tag LIKE '%test%'); DELETE from tag where tag LIKE '%test%'; DELETE from link where link LIKE '%test%';
    
    You should now be able to run the test script again.

Conclusion

You should now have a FastAPI API running with five GET endpoints and three POST endpoints, that call into a MariaDB database to retrieve and insert data.

To add DELETE endpoints, see my follow-on post, Creating an API with python: part 4: DELETE Endpoints.

Thanks for reading!