Fullstack Application

Coming Soon (August 2022)

Create a site to create and keep diary entries to learn how Cdev can be used to create and manage full stack web applications.

In this tutorial, we will be going through the entire process of creating a full stack application with Cdev. We will be focusing on demonstrating how Cdev can integrate with standard Python tools and other development workflows. We will explain all the components and steps of this tutorial in depth, but it does help to have some familiarity with some of the technologies and concepts around full stack development.

  • How the web works
  • Basics of Frontend Development (HTML, CSS, and JS)
  • Basics of React
  • Basics of Backend Development (requests, etc)
  • Relational DB technologies (Sql)
  • Monitoring Applications

Technologies

  • Backend
    • Serverless Functions
  • Database
    • Postgres
    • Sql Alchemy
    • Alembic
  • Frontend
    • React
    • Bootstrap

Create Cdev Project

We will be starting this tutorial from the standard quick-start template.

cdev init diary-project --template quick-start

Now we can deploy our project to get a live REST Api

cdev deploy

This step should output the live url of your webhook and look like

Base API URL -> <your-endpoint>

Create Database Connection

For this project we will be utilizing a relational database and the SqlAlchemy ORM. An in-depth example of how to set up SqlAlchemy ORM can be found in the examples section of our website as a part of the Integrate RelationalDBs chapter.

Update your src/hello_world/resources.py file to:

    
# Generated as part of Quick Start project template import json import os # from sqlalchemy import select, create_engine # from sqlalchemy.orm import Session from cdev.resources.simple.api import Api from cdev.resources.simple.xlambda import simple_function_annotation from cdev.resources.simple.relational_db import RelationalDB, db_engine from cdev import Project as cdev_project # from .models import Entry myProject = cdev_project.instance() ## Routes DemoApi = Api("demoapi") hello_route = DemoApi.route("/hello_world", "GET") ## DB myDB = RelationalDB( "demo_db", db_engine.aurora_postgresql, "username", "password", "default_diaryDB" ) ## Functions cluster_arn = os.environ.get("CLUSTER_ARN") secret_arn = os.environ.get("SECRET_ARN") database_name = os.environ.get("DB_NAME") # engine = create_engine(f'postgresql+auroradataapi://:@/{database_name}', # connect_args=dict(aurora_cluster_arn=cluster_arn, secret_arn=secret_arn)) @simple_function_annotation("hello_world_function", events=[hello_route.event()], environment={"CLUSTER_ARN": myDB.output.cluster_arn, "SECRET_ARN": myDB.output.secret_arn, "DB_NAME": myDB.database_name}, permissions=[myDB.available_permissions.DATABASE_ACCESS, myDB.available_permissions.SECRET_ACCESS]) def hello_world(event, context): print('Hello from inside your Function!') # session = Session(engine) # stmt = select(Entry).where(Entry.title == 'test entry') # for entry in session.scalars(stmt): # print(entry) return { "status_code": 200, "message": "Hello Outside World!" } ## Output myProject.display_output("Base API URL", DemoApi.output.endpoint) myProject.display_output("Routes", DemoApi.output.endpoints)

Now we can deploy our new resources.

cdev deploy


Next, we are going to create our database models. Install sqlalchemy_aurora_data_api.

Database models
Database models determine the logical structure of your database. They determine how data can be stored, organized, and manipulated.

pip install sqlalchemy_aurora_data_api

Then, create a src/hello_world/models.py file and add the following code:

    
from sqlalchemy import Column from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy.orm import declarative_base import sqlalchemy_aurora_data_api sqlalchemy_aurora_data_api.register_dialects() Base = declarative_base() class Entry(Base): __tablename__ = "entries" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(300)) content = Column(String(500)) def __repr__(self): return f"Content(id={self.id!r}, title={self.title!r}, content={self.content!r})"

Now we are going to install and configure alembic. Alembic is a database migration tool for usage with SQLAlchemy for Python.

pip install alembic
Alembic Library
Alembic is one of the best in class tools for working with SqlAlchemy. You can learn more about the tool from their official documentation.

Initialize the needed files for alembic using the following command. We will need to edit some of the generated files to connect to our db.

alembic init src/alembic

Replace the code in your src/alembic/env.py with the following:

    
from logging.config import fileConfig from sqlalchemy import engine_from_config from sqlalchemy import pool from alembic import context from src.hello_world.models import Base # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, # can be acquired: # my_important_option = config.get_main_option("my_important_option") # ... etc. def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL and not an Engine, though an Engine is acceptable here as well. By skipping the Engine creation we don't even need a DBAPI to be available. Calls to context.execute() here emit the given string to the script output. """ url = config.get_main_option("sqlalchemy.url") context.configure( url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, ) with context.begin_transaction(): context.run_migrations() import os from sqlalchemy import create_engine cluster_arn = os.environ.get("CLUSTER_ARN") secret_arn = os.environ.get("SECRET_ARN") database_name = os.environ.get("DB_NAME") postgres_database_engine = f'postgresql+auroradataapi://:@/{database_name}' #mysql_database_engine = f'mysql+auroradataapi://:@/{database_name}' def run_migrations_online() -> None: """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ connectable = create_engine( postgres_database_engine, echo=True, connect_args=dict(aurora_cluster_arn=cluster_arn, secret_arn=secret_arn) ) with connectable.connect() as connection: context.configure( connection=connection, target_metadata=target_metadata ) with context.begin_transaction(): context.run_migrations() if context.is_offline_mode(): run_migrations_offline() else: run_migrations_online()

Now to make sure our values are registered in the env.py script, we need to set our environment variables.

export SECRET_ARN=$(cdev output --value hello_world_comp.relationaldb.demo_db.secret_arn)
export CLUSTER_ARN=$(cdev output --value hello_world_comp.relationaldb.demo_db.cluster_arn)
export DB_NAME=<db_name>

You can now create automated migrations.

Auto Generation limits
You should familiarize yourself with the limits of alembic auto generation and ALWAYS confirm the changes before applying them.
alembic revision --autogenerate -m "Added entries table"

Apply the migration with the upgrade command. This will create our Entry Table.

alembic upgrade head


Lets now connect to our DB and add an User. The following command will open an interactive session to the database that allows you to execute SQL statments.

cdev run relationaldb.shell hello_world_comp.demo_db

Run the following commands to add a diary entry to your database.

' (table_name) => ' BEGIN
' (table_name) => 'INSERT INTO entries(title, content) VALUES ('test entry','test our db connection');
' (table_name) => 'COMMIT
' (table_name) => 'quit


Create Serverless Functions

Now that the database is set up, it is time to add some serverless functions. We can start by uncommenting lines 5-6, 13, 43-44, and 53-58 of your src/hello_world/resources.py file.
Then deploy the changes.

cdev deploy

Run the deployed function.

cdev run function.execute hello_world_comp.hello_world_function

Check the logs from the function

cdev run function.logs hello_world_comp.hello_world_function

Next, we will add the routes and functions to create and retrieve our diary entries. First, we will need to add a src/hello_world/utils.py file and add the following code to it.

    
from typing import Union, Optional, Dict def Response( status_code: Optional[int] = 200, body: Optional[Union[str, bytes, None]] = None, content_type: Optional[str] = "application/json", headers: Optional[Dict] = {}, isBase64Encoded: Optional[bool] = False, ) -> Dict: """_summary_ Args: status_code: int Http status code, example 200 content_type: str Optionally set the Content-Type header, example "application/json". Note this will be merged into any provided http headers body: Union[str, bytes, None] Optionally set the response body. Note: bytes body will be automatically base64 encoded headers: dict Optionally set specific http headers. Setting "Content-Type" hear would override the `content_type` value. Returns: Dict: _description_ """ headers.update({"content-type": content_type}) return { "statusCode": status_code, "body": body, "headers": headers, "isBase64Encoded": isBase64Encoded, }

We will then add the following to our src/hello_world/resources.py file on line 14.

from .utils import Response

Next, we need to add the following lines below the hello_route on line 24 of src/hello_world/resources.py so that our functions will have routes to be associated with.

    
create_entries_route = DemoApi.route("/entry/create", "POST") get_entries_route = DemoApi.route("/entry/get", "GET")

We can then add the function for creating entries below the hello function.

    
@simple_function_annotation("create_entry_function", events=[create_entries_route.event()], environment={"CLUSTER_ARN": myDB.output.cluster_arn, "SECRET_ARN": myDB.output.secret_arn, "DB_NAME": myDB.database_name}, permissions=[myDB.available_permissions.DATABASE_ACCESS, myDB.available_permissions.SECRET_ACCESS]) def create_entry(event, context): print('Hello from inside your entry creation Function!') session = Session(engine) data = json.loads(event.get("body")) try: session.add(Entry(title=data.get("title"), content=data.get("content"))) session.commit() return Response(200, body = json.dumps({"message": "Created entry"})) except Exception as e: print(str(e)) return Response(400, body = json.dumps({"message":"entry creation failed"}))

We can then add the entry serializer and function for retrieving all the entries below our create function.

    
def entries_serializer(obj_list): data = [] for object in obj_list: dictionary = { 'id':object.id, 'title':object.title, 'content':object.content } data.append(dictionary) print("data complete") return data @simple_function_annotation("get_entries_function", events=[get_entries_route.event()], environment={"CLUSTER_ARN": myDB.output.cluster_arn, "SECRET_ARN": myDB.output.secret_arn, "DB_NAME": myDB.database_name}, permissions=[myDB.available_permissions.DATABASE_ACCESS, myDB.available_permissions.SECRET_ACCESS]) def get_entriess(event, context): print('Hello from inside your get entriess Function!') session = Session(engine) try: entries: Entry = session.query(Entry).order_by('id').all() data = entries_serializer(entries) return Response(200, body=json.dumps(data)) except Exception as e: return Response(400, body=json.dumps({"message": str(e)}))

Then deploy the changes.

cdev deploy


Create and Connect React Frontend

To create a React application you will need to install Node.js, a JavaScript Runtime.

Node.js Installation Instructions

Windows and MacOS

Instructions for Windows and MacOs can be found here: Node JS Installers.

Windows Subsystem for Linux(WSL)

WSL instructions can be found here: Install Node.js on Windows Subsystem for Linux(WSL).

The first step is to create a frontend folder in our project to hold our React app. After creating the frontend folder run the following commands.

cd frontend
npx create-react-app <your-app-name-here>

Once the app is initialized run the following commands.

cd <your-app-name-here>
npm start

We can then update our App.js file in the src folder of our React app to the following.

    
import './App.css'; import React, {useState, useEffect} from "react"; function App() { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [entries, setEntries] = useState([]); useEffect(() => { const getEntries = async() => { const entry_Data = await fetch( "<your-backend-api-endpoint-here>/entry/get", { headers: { "Content-Type": "application/json" }, } ); const entryData = await entry_Data.json(); console.log(entryData, "this one") setEntries(entryData) console.log(entries) } getEntries(); }, []); const handleSubmit = (e) => { e.preventDefault(); console.log("function running") const createEntry = async () => { try { const entryData = await fetch( "<your-backend-api-endpoint-here>/entry/create", { method: "POST", headers: { 'Content-Type': 'application/json', }, body:JSON.stringify({ "title":title, "content":content }) } ); const entry_info = await entryData.json(); console.log(entry_info); } catch (e) { console.log(e.message); } window.location.reload(true); }; createEntry(); } return ( <div className="App"> <form onSubmit={handleSubmit}> <h2>Create New Entry</h2> <div className="form-group col"> <input className="form-control org-input px-1 pr-1" type="text" htmlFor="title" aria-label="Title" name="title" placeholder="Title" // following two lines update the state with the inputs from the user value={title} onChange={event => setTitle(event.target.value)} /> </div> <div className="form-group col"> <input className="form-control org-input px-1 pr-1" type="text" htmlFor="content" aria-label="Content" name="content" placeholder="Content" // following two lines update the state with the inputs from the user value={content} onChange={event => setContent(event.target.value)} /> </div> <button className="btn btn-success org-input px-1 pr-1 mr-2" type="submit" > SUBMIT </button> </form> <div> <h2>Entries</h2> {entries.length>0 && entries.map((item, i) => <div key={i}> <h3>{item.title}</h3> <p>{item.content}</p> </div> )} </div> </div> ); } export default App;

You will need to update the api calls with your API enpoint. At this point you can test your functions on the live server.

Connecting to the Frontend URL

Prepare the React application by creating a build folder with the following command:

npm run build
React Build
The command, npm run build, minifies files to create a production build of the React in order to reduce overall load and render times on the client-side.

In the root directory, within the src folder, create a folder named content.


Use the following command to copy the contents of the React app build folder to the root src/content folder.

Change directory in terminal
Make sure you have changed back to your root directory in your terminal before doing the following command, otherwise your terminal will not be able to find the appropriate folders/files. If you are still inside your React app folder you can use the command cd .. twice to return to your root directory for the project.

cp -r frontend/<your-app-name-here>/build/* src/content

Go to src/hello_world/resources.py and add the following code on lines 15-16

from cdev.resources.simple.static_site import StaticSite
myFrontend = StaticSite("demofrontend", content_folder="src/content", index_document='index.html')

You also need to add the following line at the bottom of your src/hello_world/resources.py file

myProject.display_output("Static Site URl", myFrontend.output.site_url)

Now save and deploy the changes.

cdev plan
cdev deploy

Push the React Application to Your Site

Sync the front-end, static React app, to the Static Site with the following command:

cdev run static_site.sync hello_world_comp.demofrontend  --dir src/content

Use the command below to get the url of the Static Site.

cdev output hello_world_comp.staticsite.demofrontend.site_url

Copy and paste it into the browser to view the Static Site, and you have successfully created a full-stack web application that you can access from any device with internet.