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.yamlfile 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/modelsdirectory, 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
uvicornandcreate-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
// @flowat the top of JSX files to enforce type safety, and runstylo build --flowto 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-mgmtpip 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/modelsdirectory, seebackend/db/models/user.pyand SQLAlchemy Tutorials for reference - Use Alembic to generate your migration
stylo db --gen-migrations "message"if usingstylobate-mgmtalembic revision --autogenerate -m "message"from thebackenddirectory if not
- Use Alembic to run your migration
stylo db --upif usingstylobate-mgmtalembic upgrade headfrom thebackenddirectory if not
- Explore your DB!
stylo db --shellif usingstylobate-mgmtsqlite3 the.dbfrom thebackenddirectory 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.pyfor 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.Okayfor consistency of return shape - Failures should be returned by raising exceptions of type
utils.api.Failurefor 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
routerobject (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-endif usingstylobate-mgmtuvicorn main:app --reloadfrom thebackenddirectory 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-endif usingstylobate-mgmtyarn startfromfrontenddirectory 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-devordocker-compose -f dev-compose.yaml build
- Run Containers
stylo run --docker-devordocker-compose -f dev-compose.yaml up -d- The
-dargument 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-devordocker-compose -f dev-compose.yaml logs -f
- Stop Containers
stylo stop --docker-devordocker-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 buildfrom thefrontenddirectory 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-devargument with--docker-prodif usingstylobate-mgmt - Replace
-f dev-compose.yamlwith-f prod-compose.yamlotherwise - 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.orgwith your domain innginx/ssl.conf - Then run
certbot/init-letsencrypt.sh 'example.org www.example.org' [email protected]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 buildfrom thefrontenddirectory 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-devargument with--docker-sslif usingstylobate-mgmt - Replace
-f dev-compose.yamlwith-f prod-compose.yamlotherwise - 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 infodbmigrations: 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
Userclass inheriting from the SQLAlchemy declarative_base, as well asUserBaseType,UserCreateType, andUserTypeclasses 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.Okayor raise autils.api.Failedto standardize return types auth.py: Sample service that implements a simple login interface, with the following endpoints:/api/auth/login,/api/auth/validate_tokenand/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-appREADME.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.jsHome.test.js: Test code forHome.js