A starting point for quick-to-implement, easy-to-deploy and lightweight web applications.
- Design Philosophy
- Design Antiphilosophy
- Infrastucture Stack
- Features
- Requiremesnts
- Getting Started
- File Structure
Generally in putting this together I was following the following principles:
Most important of all, I will be using this for a variety of random side projects. The majority of these I won't necessarily finish, or even bring to a usable state, but the building by itself is fun, and with that in mind, I wanted to make it as easy to develop on as possible. Specifically, I wanted the following:
- Clean organization: I want things to go in sensible places. Backend code goes in
backend
, frontend goes infrontend
, etc. - Hot Reloading: no compilation while in development
- Flexible languages: Python and JS are easy to quickly do stuff with, for a variety of reasons
- Optional Type Checking: Type checking sometimes makes code a lot clearer and easier to reason about, and sometimes it is a bit of an overhead. With Python Type Hints and optional Flow, I can make the choice whether I want to include it or not
While I won't necessarily finish projects that I start with Stylobate, I would like to at least be able to deploy them. I don't want to spend a bunch of effort to do that though, as it is pretty orthogonal to most things I would build. Hence: Dockerization.
Most of my deployments are going to be on my Raspberry Pi or a very cheap VM, so I want to reduce computation where possible and make it light where possible. Hence the SQLite
When putting this together, I specifically did NOT prioritize:
- Testing:
- I believe quality automated integration- and unit-testing is necessary for good, maintainable code.
- I also almost never write tests for personal projects, because most of the time I am not aiming for maintainable code as much as fun code
- So I haven't built up the infrastructure for testing very much. It should be relatively easy to add on to FastAPI and Create React App
- Strict Correctness
- Strict type checking and exhaustive error handling are also very nice to have for good, maintainable code
- They are also more work, though, so: have not spent much effort on this
I built this project as a quick jumping off point for whatever webapp I want to use. It integrates the following technologies, in rough order from back to front:
- Docker/Docker Compose
- NginX
- LetsEncrypt
- Sqlite
- Alembic
- SqlAlchemy
- Gunicorn
- ASGI
- Uvicorn
- Python 3.8
- FastAPI
- OpenAPI/Swagger
- Pydantic
- ES6/JSX
- create-react-app
- react-router-dom
- MaterialUI
- Font Awesome
That looks like a lot of technologies, but don't be afraid! All you need to get this working is to know a little bit of Python and a little bit of React.
Lots of fun features are built in!
- Management Script
- Rather than learn all the syntax of the various programs used, you can just use the stylobate-mgmt script to manage things
- Inspired by Django and Flask's
manage.py
- Production Ready
- Just build the Dockerfile.prod image and deploy it:
stylo build --docker-ssl && stylo run --docker-ssl
- Just build the Dockerfile.prod image and deploy it:
- Built-in HTTPS
- Requires using the
ssl-compose.yaml
file for docker-compose
- Requires using the
- Containers
- Development, Production, and Production-With-SSL docker-compose.yaml files provided
- Non-Containers
- Containers can be annoying and confusing and you don't have to use them
- Auto-generated migrations
- Curtesy of Alembic/SqlAlchemy, simply define the your models in the
backend/db/models
directory, and let Alembic figure out what you made or how you changed things - Generate new migrations with
stylo db --gen-migrations "some message"
, run them withstylo db --up
- Curtesy of Alembic/SqlAlchemy, simply define the your models in the
- Hot Reloading
- In development, using
uvicorn
andcreate-react-app
, backend and frontend code is reloaded on the fly when you edit things, and mostly just works
- In development, using
- Type Verification
- Build the inputs and outputs with Pydantic models and you have a bunch of cool verification built in!
- Include
// @flow
at the top of JSX files to enforce type safety, and runstylo build --flow
to verify
- Endpoint Authorization
- Just include
auth = Depends(UserAuth))
in your endpoint argument list to require user authorization, and use<MyQuery authRequired>
in the JSX to send the authorization from the backend
- Just include
- API Documentation
- Curtesy of FastAPI, Swagger API documentation is automatically produced and served at [url]/docs
- Additionally, this documentation includes code for manually triggering api endpoints, which is convenient for manual testing
- Prettyness
- Just use the MaterialUI library to get pretty and useful UX elements
- Python 3.8
- Some lower versions may work, but this uses a lot of recent features
- Node 10
- Yarn
npm install -g yarn
- Optional: Docker/docker-compose
- If you want to run the containerized server
- Optional:
stylobate-mgmt
pip install stylobate-mgmt
- Makes a lot of the management process a bit simpler
- Optionally uses the GitHub CLI to fork this repo, follow these instructions to install
To get started, simply fork this repo!
If you installed the stylobate-mgmt tool and the GitHub CLI, simply run stylo init <newproject>
- Define your models using SQLAlchemy structures in the
backend/db/models
directory, seebackend/db/models/user.py
and SQLAlchemy Tutorials for reference - Use Alembic to generate your migration
stylo db --gen-migrations "message"
if usingstylobate-mgmt
alembic revision --autogenerate -m "message"
from thebackend
directory if not
- Use Alembic to run your migration
stylo db --up
if usingstylobate-mgmt
alembic upgrade head
from thebackend
directory if not
- Explore your DB!
stylo db --shell
if usingstylobate-mgmt
sqlite3 the.db
from thebackend
directory if not
- Cluster endpoints in "services"
- If a service is relatively straightforward, define it in
backend/services/<service>.py
- If it would make sense to spread helping functions across multiple files, define it in
backend/services/<service>/main.py
- Implement endpoints as decorated functions
- See
backend/services/user.py
for examples, and FastAPI documentation for reference - Define input/output models inheriting from Pydantic's BaseModel for nice autogenerated documentation served at [host]:[port]/docs
- Output models should do this by inheriting from
utils.api.Okay
for consistency of return shape - Failures should be returned by raising exceptions of type
utils.api.Failure
for the same reason
- Output models should do this by inheriting from
- add a dependency of
Depends(UserAuth)
for endpoints that require a user be logged in, andDepends(SuperUserAuth)
for endpoints that require a superuser
- See
- From each service, export 1
router
object (this object is used to decorate the endpoints) - Add the router to the main application in
backend/main.py
- Follow the pattern used by the auth service:
app.include_router(auth.router, prefix='api/auth', tags=['auth'])
- If it isn't included, it will not respond to requests
- The prefix must include api if it is to work with the frontend
- The prefix should also include the name of the service
- The tags are used to organize the documentation; by adding this to the router as a whole, it ensures the documentation is organized around services
- Follow the pattern used by the auth service:
- Run the backend server!
stylo run --back-end
if usingstylobate-mgmt
uvicorn main:app --reload
from thebackend
directory if not (the reload parameter enables hot reloading)- It will be listening at localhost:8000 by default
- Documentation of all endpoints will be visible at localhost:8000/docs
- It is additionally possible to trigger endpoint calls from within that documentation, which is very convenient
- Test out calls, either in localhost:8000/docs or from curl/Postman
- If using Postman, you can export the api by downloading the OpenAPI specification at locatlhost:8000/openapi.json and then import it!
- Define your routes within
frontend/src/routes.jsx
- Add new pages within
frontend/src/pages
- Add new common components within
frontend/src/components
- Add package dependencies with
yarn add <x>
- Ensure Ergonomics
- For cleanest implementation:
- Use Functional components instead of Classy components
- Instead of relying on Redux, use React.useContext and its Provider/Consumer model
- For cleanest implementation:
- Add utility functions within
frontend/src/utils
- Add tests wherever you want, I think??
- Run the development server!
stylo run --front-end
if usingstylobate-mgmt
yarn start
fromfrontend
directory if not- Probably only want to do this while also running the backend server, so it has someone to talk to
There are a few alternatives set up for running in docker-compose.
If you wish to run docker in development, use the dev-compose.yaml
docker-compose structure. This will mount your frontend and backend directories, allowing for hot-reloading. This runs the Uvicorn server for the backend and the React-Scripts server for the frontend.
- Fix proxies
- The React Server is by default set up to proxy API requests to localhost:8000, but after it is put in an individual container that is no longer accessible
- Replace
"proxy": "http://localhost:8000",
with"proxy": "http://backend:8000",
infrontend/package.json
- Not required if using
stylobate-mgmt
, it handles that for you
- Build Images
stylo build --docker-dev
ordocker-compose -f dev-compose.yaml build
- Run Containers
stylo run --docker-dev
ordocker-compose -f dev-compose.yaml up -d
- The
-d
argument makes the containers run in the background - The frontend will be accessible at localhost:3000, the backend at localhost:8000
- Follow logs
stylo logs --docker-dev
ordocker-compose -f dev-compose.yaml logs -f
- Stop Containers
stylo stop --docker-dev
ordocker-compose -f dev-compose.yaml down
This runs NginX as the base web server, directly serving compiled static files for the frontend and proxying requests to Gunicorn-managed Uvicorn processes on the backend. There are no volumes mounted, so the server only needs the nginx and backend docker images.
- Build JS
- NginX needs the latest compiled files from the frontend project, so run
yarn build
from thefrontend
directory before building the images - Not required if using
stylobate-mgmt
, it handles that for you
- NginX needs the latest compiled files from the frontend project, so run
- Build Images, Run Containers, Follow Logs, and Stop Containers
- Same steps as for Docker in Development!
- Replace
--docker-dev
argument with--docker-prod
if usingstylobate-mgmt
- Replace
-f dev-compose.yaml
with-f prod-compose.yaml
otherwise - Everything will be served through localhost:80, with API calls to localhost:80/api/[endpoint] forwarded to the FastAPI server
Same basic deal as Docker in Production, but with SSL Certificates served by NginX and managed by LetsEncrypt's Certbot
- Set Up Certificates
- First off, you need to have a domain handy for which LetsEncrypt will generate certificates
- Bootstrapping LetsEncrypt containers from within a server is a bit complicated, but there is a script for that!
- Not required if using
stylobate-mgmt
, it handles that for you - Otherwise, replace the references to
example.org
with your domain innginx/ssl.conf
- Then run
certbot/init-letsencrypt.sh 'example.org www.example.org' email@example.org
from the top directory - It is only necessary to do this the first time you build, otherwise things should be managed for you
- Build JS
- NginX needs the latest compiled files from the frontend project, so run
yarn build
from thefrontend
directory before building the images - Not required if using
stylobate-mgmt
, it handles that for you
- NginX needs the latest compiled files from the frontend project, so run
- Build Images, Run Containers, Follow Logs, and Stop Containers
- Same steps as for Docker Development/Docker Production
- Replace
--docker-dev
argument with--docker-ssl
if usingstylobate-mgmt
- Replace
-f dev-compose.yaml
with-f prod-compose.yaml
otherwise - Everything will be served through localhost:443, with requests to localhost:80 automatically redirected
Detailed documentation about how everything is layed out. I thought of this as a relatively simplistic web server before I wrote this out. I guess it still is? It makes sense to me, at least
backend
: backend Python code, configuration, and DB stuffmain.py
: main entry point, register all the "services" heresql.py
: Initializes the DB connectionthe.db
: The SQLite Database (TODO: move this elsewhere/maybe use PostgreSQL)alembic.ini
: Alembic (migrations) configuration, mostly for logging, documentation hererequirements.txt
: the python requirements- it is recommended to install these within a virtualenv:
python -m venv venv; source venv/bin/activate; pip install -r requirements
- it is recommended to install these within a virtualenv:
Dockerfile.dev
,Dockerfile.prod
: Simple dockerfiles to get up and running, see Run in Docker for more infodb
migrations
: Alembic folder, not strictly necessary to touch unless to modify auto-generated migrations (or write your own)versions
: the actual migrationsenv.py
: sets up the Alembic environmentscript.py.mako
: File generated by Alembic, don't actually know what this does
models
: Define all your Database models here__init__.py
: import from your models files for ease of useuser.py
: Sample model file- Note there is both a
User
class inheriting from the SQLAlchemy declarative_base, as well asUserBaseType
,UserCreateType
, andUserType
classes extending the PydanticBaseModel
. The former defines the relations with SQL, the latter serve as data-classes for reading/selecting/writing from the database
- Note there is both a
services
: Yes, it is stretching the definition a bit, but basically I am using the term "service" to refer to an organization of endpoints, each of which would have the same prefix in the URL- Each service should serve to define a
utils.api.StyloRouter
, which then needs to be added inmain.py
- This will make sure the endpoints get appropriate paths and the service is appropriately documented
- Each endpoint should be defined as a decorated function in a service
- Each endpoint should either return a
utils.api.Okay
or raise autils.api.Failed
to standardize return types auth.py
: Sample service that implements a simple login interface, with the following endpoints:/api/auth/login
,/api/auth/validate_token
and/api/auth/info
- Each service should serve to define a
utils
: Utility scripts whose functionality will be used across services or other parts of the serverapi.py
: Helpers for endpoint definitionsconfig.py
: Defines configuration variables and defaults to be used across the system
frontend
: Front-end project, created withcreate-react-app
README.md
: autogenerated but helpful commands for managing development instances of the front end application.flowconfig
: Configuration for flow, documentation herepackage.json
: Node configuration, consisting mainly of dependencies, scripts, and compilation targetsyarn.lock
: detailed list of dependenciesDockerfile.dev
: Simple dockerfile to get up and running, see Run in Docker for more infobuild
: Compiled static files go here. In production mode, this directory is statically mounted and served at the base directorynode_modules
: All of the files thatpublic
: Static resources, which get moved to the build dir when compiledsrc
: Well the source code duhindex.css
: general styling applied to the siteindex.js
: The entrypointRutes.jsx
: React-dom-router for managing different urlsserviceWorker.js
: Auto-generated code for a serviceWorker to make the app work offline... if you want?setupTests.js
: Code called before running test codesutils
: utility code to be used across pagesAuthContext.jsx
: Context provider for managing authentication state. Ideally minimal interaction with this is necessaryQuery.jsx
: Helper components for dealing with general requests (<Query>
) and api calls (<MyGet>
,<MyPost>
, etc)
components
: components used across multiple pagespages
: Individual pageshome
: Useless sample home page and required resourcesHome.js
: Code that will always be runningHome.css
: stylizesHome.js
Home.test.js
: Test code forHome.js