Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Building a REST API in Python

In this lab, you will build a Python-based REST API service to manage products. You'll learn to define data models, configure the FastAPI environment, and implement core REST operations such as creating, reading, updating, and deleting products. Additionally, you'll explore best practices for designing and testing APIs.

Lab platform
Lab Info
Level
Beginner
Last updated
Dec 22, 2025
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Welcome to the lab

    Welcome to the lab

    Guided: Building a REST API in Python

    Introduction

    Welcome to the lab on building REST APIs with Python! In this lab, you'll learn the fundamentals of creating RESTful APIs, a key component in modern web development.

    REST APIs allow software systems to communicate with each other over the web, enabling seamless integration and data exchange.

    Core REST API

    In this lab, you'll implement RESTful principles by creating an API service designed to manage products. This service will allow users to perform operations such as adding new products, retrieving existing products, updating product details, and deleting products. The core CRUD (Create, Read, Update, Delete) operations, which are the backbone of REST APIs.

    Testing with OpenAPI Documentation

    You'll leverage FastAPI's integrated Swagger UI to test the API during development. This powerful tool simplifies the process of designing, documenting, and validating REST APIs, ensuring your endpoints are well-documented and function as expected.

    By the end of this lab, you'll have the foundational knowledge and skills to build and test REST APIs effectively.

    If you encounter any challenges, feel free to consult the /solution directory for guidance.

    To begin, click the right-arrow below.

  2. Challenge

    Define Validation Schema

    Define Validation Schema

    To start, define the schema that will act as the foundation of the project.

    This schema will outline the structure of the data you will manage, ensuring consistency and validation throughout the API.

    Using Pydantic

    Ensuring data validation and consistency is crucial when developing APIs. Pydantic, a robust Python library, simplifies this process by allowing you to define input and output data structures using type annotations.

    For this project, you will define a Product schema based on Pydantic's BaseModel.

    To accomplish this, open the /schema.py file and add the following definition:

    class ProductCreate(BaseModel):
        name: str
        description: str
        price: Decimal
    
    class ProductRead(ProductCreate):
        id: int
    

    Create and Read Classes

    The ProductCreate class extends Pydantic's BaseModel, enabling data validation and serialization.

    The id field is dynamically generated when a new product is created. Therefore, the create product endpoint should not require an id field.

    All the other endpoints deal with existing products, which need to be managed by a product id field. This requirement is handled by the ProductRead class, which extends ProductCreate by adding an id field.

    This design allows for creating products without an id and retrieving products with an id.

    Adding Constraints

    To ensure data integrity, Pydantic provides various validators that can be used to enforce these constraints. Validators are functions that check whether the data meets specific criteria.

    Pydantic enforces type validation by default and ensures that every declared field has a value. However, you can configure additional validation rules as needed.

    To see this in action, scroll to the bottom of the schema.py file to the main guard block. Here, you will find an example of how to create a ProductCreate instance though currently the price field is commented out.

    You can test the validation, by running this file with the following command:

    python schema.py
    

    When you run the script, Pydantic will raise a ValidationError, with a message that the Field required .... This demonstrates how Pydantic automatically validates data types based on the defined schema.

    You could uncomment the price field and run the script again. This time, you will see a different error message indicating that the Input should be a valid decimal.

    Only when a valid price value is provided, will the instance be created successfully.

    Other Validators

    Currently, this schema does not prevent a negative price, which is unsuitable for a product. To address this, you can use Pydantic's Field method to define constraints and metadata.

    To achieve this, refactor the price field to be defined by Pydantic's Field function, which allows you to specify additional validation rules:

    price: Decimal = Field(..., gt=0)
    

    Now if you set the price to a negative value, Pydantic will raise a ValidationError, indicating that the Input should be greater than 0.

    Pydantic offers a wide range of validators, making it a valuable tool for enforcing rules on submitted data. Exploring these validators further can enhance your understanding and ensure robust data validation.

    With the Product schema defined and basic constraints in place, you are now ready to proceed with declaring the data storage.

    Click the right arrow below to continue.

  3. Challenge

    The API Environment

    The API Environment

    With the schema declared, you can begin setting up the API environment. This involves creating a FastAPI application and configuring it to handle incoming requests.

    Using the FastAPI framework

    FastAPI is a modern web framework for building APIs with Python. It is designed to be fast, easy to use, and highly performant.

    Installing Libraries ----- The required libraries for this project are pre-installed in this environment. If you are setting up a different environment, you can install FastAPI and Uvicorn (an ASGI server) using pip: ```bash pip install fastapi uvicorn ```

    Open the /app.py file to review the basic setup of a FastAPI application. This serves as the foundation for developing your API.

    Notice the predefined root endpoint decorated with @app.get("/"). You'll use this endpoint to test the API and ensure it's running correctly.

    Running the FastAPI application

    You can use the Uvicorn server to start and run this FastAPI application.

    What is Uvicorn ----- Uvicorn is an ASGI server used to run FastAPI applications.

    ASGI (Asynchronous Server Gateway Interface) is a specification for asynchronous communication between Python web servers and applications, enabling high-performance and scalable web apps.

    Opening the /main.py file, you will find the call to uvicorn.run(). This function is responsible for starting the server and running the FastAPI application.

    Reload Parameter ----- Notice that the `reload` parameter is set to `True`. This enables the application to automatically reload when changes are made to the code, which is particularly useful during development.

    To launch the server, run the following command in your terminal:

    python main.py
    

    You should see output indicating that the server is running.

    Reviewing the API

    Once the API is running, navigate to the Simple Browser tab and click the circular reload icon next to the address bar. This url opens the /docs path which is the default location for the Swagger UI documentation generated by FastAPI.

    External Browser -----

    If you prefer to view the API documentation in a separate browser tab, click the icon with a box and an arrow located to the right of the address bar.

    OpenAPI Documentation

    FastAPI generates OpenAPI documentation automatically, which is displayed in this Swagger UI. This documentation offers a comprehensive view of your API's endpoints, request parameters, and response formats, along with the ability to test endpoints directly within the interface.

    To test an endpoint, expand its row by clicking on it. Next, click the Try it out button, provide any necessary parameters, and press Execute. This will send a request to the API and display the response directly within the documentation interface. A successful 200 response should return a JSON body containing the message field.

    Now that the API is launched, you can begin building out your endpoints and implementing the necessary functionality.

    Proceed by clicking the right arrow below.

  4. Challenge

    Simulating a Database

    Simulating a Database

    Interacting with a database is a common task in web development, and FastAPI provides excellent support for this. However, setting up a full database can be time-consuming and complex, especially during the early stages of development.

    In this project, you will simulate a database using an in-memory list to store your data. This approach allows you to focus on building your API without the overhead of managing a database.

    This functionality has been provided for you in the /services.pyfile.

    By separating this functionality into a temporary service layer, you can easily switch to a real database later without affecting the rest of your code. This modular approach is a best practice in software development, allowing for easier maintenance and scalability.

    Service Layer

    This service layer is designed to handle the CRUD operations for your products. It provides methods for creating, reading, updating, and deleting products from the in-memory list.

    In the /app.py file, you will find that the service variable is initialized with an instance of the ProductServices class. This instance serves as the interface for interacting with the database throughout the API.

    With these supporting files in place, you are now ready to begin developing the API endpoints.

    In the next several steps you will create the API routes that will handle incoming requests and interact with the service layer to perform the necessary operations.

  5. Challenge

    Creating Products

    Creating Products

    With the data schema and service layer in place, the next step is to implement the API endpoint for creating products.

    Returning to the /app.py file, scroll to just after the @app.get("/") root endpoint. This is where you will define the new endpoint for creating products.

    Design Endpoint

    The create endpoint should receive a JSON payload adhering to the ProductCreate schema and pass it to the service.add_product() method for processing.

    Keep in mind that the ProductCreate schema is utilized here because the id field is not needed during product creation. The id will be generated and assigned by the service layer when the product is added to the database.

    Implementing the Endpoint

    FastAPI endpoints are defined as functions decorated with the appropriate HTTP method.

    To implement the create product endpoint, add the following function just below the root endpoint:

    @app.post("/products/")
    async def create_product(product: ProductCreate):
        return service.add_product(product)
    

    Reviewing this code:

    @app.post("/products/"):
    This FastAPI decorator designates the function as an endpoint that processes POST requests directed to the /products/ URL.

    async def:
    This signifies that the function is asynchronous, enabling it to process requests without blocking the server. This is particularly beneficial for I/O-bound tasks such as database interactions.

    create_product(product: ProductCreate): This function takes a ProductCreate object as input, with FastAPI handling validation against the specified schema automatically.

    return service.add_product(product):
    This forwards the ProductCreate object to the add_product() method, which adds the product to the database. FastAPI then serializes the returned object into a JSON response.

    Testing the Endpoint

    To test the create_product endpoint, navigate to the Swagger UI documentation page and refresh it. This can be accessed via the Simple Browser tab or the external browser you opened earlier. You should now see the POST /products/ Create Product endpoint listed in the API documentation.

    Expanding this endpoint will display the expected request body format, which corresponds to the ProductCreate schema you defined earlier.

    You can verify the endpoint by clicking the Try it out button and entering a JSON object in the request body, such as:

    {
      "name": "Test Product",
      "description": "This is a test product.",
      "price": 19.99
    }
    

    Next, click the Execute button to send a POST request to the /products/ endpoint with the provided JSON payload.

    Upon a successful request, you will receive a response containing the created product object, including the id and other details. This confirms that the product has been successfully added to the simulated database.

    Handling Errors

    Error handling is a critical component of API design. FastAPI simplifies this by automatically validating request bodies against the defined schema.

    For example, you can test this behavior by sending a request with an invalid JSON payload, such as:

    {
      "name": "Test Product",
      "description": "This is a test product.",
      "price": "not a number"
    }
    

    When this request is executed, FastAPI validates the input and responds with a detailed error message highlighting the invalid field, accompanied by a 422 Unprocessable Entity status code.

    Improving Error Handling

    Although FastAPI automatically manages validation errors, in a real-world application, you should also handle other potential issues, such as database connection failures or unexpected server errors. FastAPI's exception handling features can help you address these scenarios effectively.

    You have completed your first endpoint for creating Products! In the next step, you will implement the endpoint for retrieving products.

  6. Challenge

    Fetch Products

    Fetch Products

    After creating a new product, the next step is to add functionality for retrieving products from the database. This involves implementing endpoints to fetch all products and to fetch a specific product by its ID.

    Design Endpoint

    To fetch all products, define a new endpoint that calls the service.get_all_products() method. This will enable your application to return a complete list of products.

    Add a GET endpoint in the /app.py file below the create_product function to retrieve all products:

    @app.get("/products/")
    async def get_products():
        return service.get_all_products()
    

    This code is similar to the previous example, but here you are using a GET request to fetch data instead of a POST request to create it.

    The @app.get("/products/") decorator specifies that this function handles GET requests to the /products/ URL.

    Testing the Endpoint

    When you refresh the Swagger UI documentation page, the GET /products/ endpoint will appear.

    Test this endpoint by selecting the Try it out button and then clicking Execute.

    The response body should initially display an empty list. This occurs because the server reloads and reinitializes the in-memory database with each update. To test with data, you will need to first call the create endpoint to add a product to the database.

    Endpoint Summary -----

    You may be wondering where the "Get Products" summary to the right of the GET /products/ endpoint comes from. This is automatically generated by FastAPI based on the function name and docstring.

    If you are interested in customizing the summary or description of your endpoints, you can do so by adding a docstring to the function. For example:

    @app.get("/products/", 
      summary="Get all products", 
      description="Fetches a list of all products in the database."
    )
    

    If you make that change and refresh the Swagger UI documentation page, you will see the updated summary and description for the GET /products/ endpoint.

    GET Product by ID

    To fetch details of a specific product using its id, create an endpoint that accepts product_id as a path parameter. This adheres to RESTful API design principles, allowing your application to retrieve detailed information about an individual product.

    Insert the following endpoint into the app.py file directly below the get_products function:

    @app.get("/products/{product_id}")
    async def get_product(product_id: int):
        return service.get_product_by_id(product_id)
    

    This endpoint is similar to the previous one, but now includes a path parameter {product_id}. The parameter corresponds to the product_id argument in the function definition. FastAPI automatically extracts this parameter from the URL and passes it to the function.

    Testing the Endpoint

    To test the get_product endpoint, refresh the Swagger UI documentation page. You should now see the GET /products/{product_id} endpoint listed.

    Keep in mind that to test this functionality, you must first create a product using the POST /products/ endpoint and note down the id of the created product.

    After creating a product, test the GET /products/{product_id} endpoint by expanding on the endpoint and clicking the Try it out button. Enter the id of the previously created product in the product_id field and click Execute.

    The response body should display the details of the specified product, confirming that the endpoint is functioning correctly.

    Congratulations! You have successfully created and tested the endpoints for retrieving products. Next, you will implement functionality to update products.

  7. Challenge

    Update Products

    Update Products

    With the ability to create and retrieve products in place, the next step is to add functionality for updating existing products.

    Design Endpoint

    To modify a product, the PUT HTTP method is used. This method is designed to update an existing resource on the server. Here, you will define an endpoint that enables updating a product's details using its id.

    Add the following code to the /app.py file, placing it directly below the get_product function:

    @app.put("/product/{product_id}")
    async def update_product(product_id: int, product: ProductCreate):
        product = service.update_product(product_id, product)
        return product
    

    By now, the FastAPI decorators should be a familiar concept. This endpoint follows a similar structure to the ones implemented earlier.

    The decorator defines the path as /product/{product_id} and sets the HTTP method to PUT. Declaring the product_id as a path parameter.

    The function accepts two parameters:

    product_id: which corresponds to the path parameter.

    product: which is of type ProductCreate. Note that this schema excludes the id field since the product_id is already provided in the path.

    The function then calls the service.update_product() method, which handles updating the product in the database.

    Testing the Endpoint

    Keep in mind that to test this functionality, you must first create a product using the POST /products/ endpoint and note down the id of the created product.

    After refreshing the Swagger UI documentation page, you will see the newly added PUT /product/{product_id} Update Product endpoint.

    By clicking the Try it out button, you can provide the product_id path parameter for the product you wish to update, along with the updated product details. Use the same product_id that you retrieved earlier to test the functionality.

    When you execute the request, the product will be updated in the database, and the response body will display the updated product details.

    Enhancing the Endpoint

    The current implementation of the update_product endpoint is minimal and does not handle cases such as when the specified product does not exist or if an error occurs during the update process.

    For instance, if you try to update a product that does not exist, the current service.update_product() implementation will return None. This results in a response with a 200 OK status and a body of null, which is not helpful to the user. You can test this behavior by passing a product_id that does not exist in the database, such as 0.

    To improve the user experience, you can modify the update_product function to raise an HTTP exception when the product is not found. This ensures the client receives a more informative response.

    FastAPI provides the HTTPException class, which allows you to return HTTP errors with a specific status code and a descriptive error message.

    You can refactor the update_product function's return statement with the following:

        if product is None:
            raise HTTPException(status_code=404, detail="Product not found")
        return product
    

    If the service layer cannot locate the product, the endpoint will now raise an HTTP exception with a 404 Not Found status code and a descriptive message stating that the product was not found. This ensures the client receives a clearer and more informative response, indicating that the product they attempted to update does not exist.

    You can rerun the PUT /product/{product_id} endpoint with a non-existent product_id to see this new error message.

    You are almost complete with the product management functionality. The last step is to implement the ability to delete products from the database.

  8. Challenge

    Delete Products

    Delete Products

    You are almost done with implementing the product management functionality. So far, you have implemented the ability to create, read, and update products. The final step is to add the functionality to delete products from the database.

    Endpoint Design

    In a REST API, the DELETE method is used to remove a specific resource identified by a unique identifier, such as product_id. Upon successful deletion, the server should return an appropriate status code, like 204 No Content, indicating the operation was successful with no additional content in the response.

    To implement this functionality, you will define a new endpoint that takes product_id as a path parameter. This endpoint will invoke the service.delete_product() method to delete the corresponding product from the database.

    Implementing the Endpoint

    Add the following code to the app.py file, placing it directly below the update_product function:

    @app.delete("/product/{product_id}", 
      status_code=status.HTTP_204_NO_CONTENT
    )
    async def delete_product(product_id: int):
        success = service.delete_product(product_id)
    

    This code is similar to the previous examples. It sets up an endpoint to handle DELETE requests at the /product/{product_id} URL. The product_id path parameter is passed to the service.delete_product() method, which deletes the specified product from the database.

    In this decorator, the status_code parameter is set to 204 No Content, which is a standard RESTful API convention indicating that the deletion was successful and there is no content in the response body.

    You can enhance this endpoint further by using the HTTPException class to handle scenarios where the specified product does not exist. This approach would be similar to how you handled such cases in the update_product endpoint.

    Summary

    Well done on completing the product management functionality! You have now implemented the full CRUD operations—create, read, update, and delete—for managing products in your application.

    This lab serves as an introduction to FastAPI's capabilities. To take your application to the next level, consider exploring advanced features like authentication, authorization, and middleware.

    Here's to your success in mastering FastAPI and Python—keep up the great work!

About the author

Jeff Hopper is a polyglot solution developer with over 20 years of experience across several business domains. He has enjoyed many of those years focusing on the .Net stack.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight