| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
This project is an exploration of modern fullstack development by building and comparing interchangeable backend APIs and frontend SPAs using different technologies.
The core idea is to replicate the same REST API and frontend application across multiple tech stacks and make them fully interchangeable.
All backends expose identical endpoints, and all frontends consume the same APIs, enabling any frontend to work with any backend without modification.
This setup allows for:
Each application (frontend/backend) is containerized using Podman and organized in its own dedicated folder:
The backend- and frontend- prefixes group related applications together for easier navigation.
The podman-compose.yaml file orchestrates the following shared services:
This project uses PostgreSQL as its primary database. While earlier iterations explored MongoDB for its document-oriented model, the project has migrated to PostgreSQL for its industry-standard adoption, superior performance, and robust feature set
PostgreSQL combines the flexibility needed for modern application development with the reliability and performance required for production systems.
Each backend implements the same logic, routes, and data models:
| Language | Web Framework | API Address |
|---|---|---|
| TypeScript | Express | http://localhost:5000/api |
| Python | FastAPI | http://localhost:5001/api |
| Go | Gin | http://localhost:5002/api |
| Rust | Axum | http://localhost:5003/api |
Each backend connects to a shared set of services (e.g., PostgreSQL, Redis).
Each frontend is a modern Single Page Application (SPA) built using popular frameworks:
| Framework | Dev Server URL | Env File Example |
|---|---|---|
| React | http://localhost:8000 | react.env |
| Vue | http://localhost:8001 | vue.env |
| Svelte | http://localhost:8002 | svelte.env |
| Angular | http://localhost:8003 | angular.env |
All frontends communicate with any backend through the same REST API, enabling a plug-and-play architecture.
To connect a frontend to a specific backend, update the corresponding .env file with the correct API URL.
For example, for React, update the VITE_BACKEND_URL in react.env like so:
VITE_BACKEND_URL=http://localhost:5000/api # Express APIor
VITE_BACKEND_URL=http://localhost:5001/api # FastAPIThis demo project features two models: Users and Places. Users can create accounts and add their favorite places to their profiles, while other users can browse and view these places.
Defining a clear folder structure is essential for building scalable applications. Poor organization quickly leads to issues such as circular import errors and difficult maintenance.
Below is the folder structure used across all backend implementations, shown from bottom to top (import order), independent of the programming language or web framework
Reusable and abstracted modules that serve as the foundation layer of the application. No imports from sibling modules are allowed, placing /lib at the bottom of the import order.
The /lib folder acts as the glue layer between the application logic and external dependencies — providing consistent interfaces for HTTP frameworks, data validation libraries, PostgreSQL ORMs, and third-party services.
Note: The underscore suffix (e.g., express_, pydantic_) distinguishes these abstraction layers from the underlying libraries they wrap.
Centralized configuration module for loading and managing environment variables and global parameters. The /config folder sits just above /lib in the import order.
This module may import utilities from /lib (e.g., for parsing, validating, or transforming environment variables) but does not import from any higher-level modules.
Typical contents:
Stores static assets such as images or files that may be served by the backend or used for documentation or testing. It may also contain helper functions for loading these assets.
/static is considered near the bottom of the import order, just above lib and config.
Manages application services such as database connections, third-party APIs, caching layers, and message queues. These modules import clients from /lib and instantiate them with project-specific configurations using parameters from /config.
The /services folder is split into two submodules to prevent circular imports:
instances – Contains instantiated service objects consumed throughout the application (e.g., database connections, Redis clients, task queue publishers, external API clients). Each instance should expose:
setup – Contains orchestration logic to manage the lifecycle of all service instances together. This module:
Import order: The instances submodule sits near the bottom (can be imported by models, routes, etc.), while setup sits at the top (only imported by the main entry point). This separation prevents circular dependencies where services need each other during initialization.
Contains the data layer — the core domain models and database interaction logic.
Well-designed data models are critical for:
The /models folder is organized into subfolders that separate concerns by lifecycle stage:
When creating or updating a model, the first thing to think about is how it is represented in the database.
The /orm folder contains the DB implementation of each model using the chosen ORM (SQLAlchemy, TypeORM, GORM, or SeaORM).
ORMs are preferred over raw SQL queries because they offer:
The /migrations folder contains database migration files that track and apply schema changes over time.
Each migration file:
Migration workflows vary by stack:
Once the database representation is settled and the database updated, the next step is defining how the model is represented in the codebase itself.
The ORM defines how data is stored, while schemas define how data flows through the application
Schema definitions are organized by their specific purpose in the application lifecycle — from creation and storage to retrieval and querying.
Each model has its own schema file containing multiple schema types:
Used for generating dummy data during database seeding. May include reference fields (e.g. _ref or _creator_ref) to link records across tables.
Represents the internal structure of a new record before insertion. Typically excludes auto-generated fields like IDs, timestamps, or reverse relationships.
Defines the API request body for HTTP POST endpoints. May differ from the Create Schema when transformation is needed (e.g., accepting a file upload that becomes an imageUrl string in storage).
Used when returning data to clients. Excludes sensitive or internal fields (e.g., password hashes, internal IDs) for security.
Describes partial updates to existing records. All fields are typically optional to allow flexible modifications.
Defines the API request body for HTTP PUT endpoints. May differ from the Update Schema when transformation is needed (e.g. raw image to imageUrl).
Note: REST conventions suggest using PATCH for partial updates, but PUT is used here due to its wider adoption and familiarity.
Defines the complete search API structure, combining filters, field selection, sorting, and pagination into a single schema.
Components of the Search Schema:
Type literals for validation:
Each model defines type literals to enforce which operations are allowed:
These literals provide type safety and runtime validation for the Search Schema.
Wraps search results with pagination metadata: total_count, page, total_pages, and the data array.
For each model, a corresponding CRUDS class is created to encapsulate all Create, Read, Update, Delete, and Search operations.
CRUDS stands for:
Why the extra "S"? Traditional CRUD's "Read" typically covers simple ID-based retrieval. The added "S" for Search represents a sophisticated query API that combines filtering, sorting, field selection, and pagination—distinguishing get(id) from complex search(query) operations.
CRUDS operations are organized into four layers:
This folder contains example records for each model, along with utility methods to seed and dump the database for testing and development.
Each example is structured using the SeedSchema defined in the schemas/ folder, which may include reference placeholders (e.g., _creatorRef) that get resolved to actual IDs during the seeding process.
Typical contents:
In a full-featured application, the /core layer contains business logic that orchestrates data operations to deliver complex functionality beyond basic CRUDS.
The Core layer uses CRUDS methods from Models and clients from Services to implement multi-step workflows, enforce business rules, and coordinate cross-model operations.
Note: This project does not include a /core folder, as it is a basic CRUDS API demonstration.
The /background folder contains code responsible for executing background jobs—tasks that run outside the scope of an API request.
These jobs often manipulate data defined in /models and apply business logic from /core.
For this reason, /background sits higher in the import order.
Some models or core logic may trigger background jobs (e.g., updating an embedding vector after a CRUDS operation). To avoid circular imports, publishers (functions that enqueue tasks) and handlers (functions that process tasks) are separated into modules that do not import each other.
A model can import a publisher to trigger a task, while a handler can import the same model to process that task—avoiding circular dependencies.
publishers and handlers share common parameters (e.g., broker URLs, task names, queue names, execution order). A bgconfig module stores these shared parameters, which both publishers and handlers import.
crons is the fourth submodule of /background. It contains scheduled tasks that periodically trigger jobs by calling a publisher.
Import order within /background:
Includes scripts for data migration, debugging, or manual testing.
This folder was initially named scripts. It was renamed to bin because Rust provides special support for executing code placed in this directory.
The /api folder contains all logic related to HTTP request handling, authentication, middleware, and API documentation.
It acts as the main entry point for routing requests to the appropriate backend logic and sits in the upper tier of the import order.
It includes the following subfolders:
Contains middleware functions that apply logic before or after route handling, including:
⚠️ Different frameworks implement this concept differently: Express/Gin use traditional middleware functions, FastAPI uses dependency injection (Depends()), and Axum uses extractors for request data and Tower middleware layers for cross-cutting concerns. Despite these implementation differences, the core concept remains the same: reusable logic that executes around route handlers and extracts data from the incoming request.
Contains code responsible for setting up the Swagger/OpenAPI documentation.
Documentation generation varies by framework:
Defines the actual REST API endpoints for each resource/data model.
Each model exposes a standardized set of 6 CRUDS endpoints, ensuring consistency across all backends:
| Method | Path | Purpose | Input Schema | Output Schema | CRUDS Method |
|---|---|---|---|---|---|
| GET | /model-name/ | Search with filters via query parameters | Query Params | PaginatedDataSchema | paginate() or userPaginate() |
| POST | /model-name/search | Search with filters via request body | SearchSchema | PaginatedDataSchema | paginate() or userPaginate() |
| POST | /model-name/ | Create a new record | PostSchema | ReadSchema | post() or userPost() |
| GET | /model-name/:id | Retrieve a single record by ID | Query Params | ReadSchema | get() or userGet() |
| PUT | /model-name/:id | Update an existing record | PutSchema | ReadSchema | put() or userPut() |
| DELETE | /model-name/:id | Delete a record by ID | – | – | delete() or userDelete() |
Note: For GET /model-name/, query params are parsed and converted into a SearchSchema via middleware. For GET /model-name/:id, optional fields query param controls which fields are returned.
The GET /model-name/ and POST /model-name/search endpoints support advanced filtering, field selection, sorting, and pagination.
Accepted query parameters:
Each filtering query parameter follows the pattern: field=operator:value
If no operator is provided, eq (equals) is assumed. For nested fields, aliases may be used (e.g., zipcode for address.zipcode).
Supported operators:
| Operator | Meaning | Example |
|---|---|---|
| eq | Equals | age=eq:30 |
| ne | Not equals | status=ne:inactive |
| null | Wether the field is null or not | status=null:false |
| in | In list | status=in:active,pending |
| nin | Not in list | role=nin:admin,moderator |
| gt | Greater than | age=gt:18 |
| gte | Greater or equal | age=gte:21 |
| lt | Less than | price=lt:100 |
| lte | Less or equal | age=lte:65 |
| like | Pattern match (SQL LIKE) | name=like:John% |
| ilike | Pattern match (SQL ILIKE) | email=ilike:.*@gmail.com |
Example GET request:
/users?fields=name,age&age=gte:30&age=lte:40&name=like:%Slim%&zipcode=2040&sort=-age
Returns:
This translates to SQL similar to:
SELECT name, age FROM users
WHERE age >= 30
AND age <= 40
AND name LIKE '%Slim%'
AND address->>'zipcode' = '2040'
ORDER BY age DESCNote: Query params are parsed and converted into a SearchSchema via middleware before being passed to the CRUDS paginate() method.
To overcome GET request limitations (URL length, lack of request body), the POST /model-name/search endpoint provides the same functionality using a JSON body.
The main advantage is to circumvent GET requests' URL length restrictions (~2,000 characters) for complex queries.
Example request body:
{
"filters": {
"age": ["gte:30", "lte:40"],
"name": ["like:%Slim%"],
"zipcode": ["2040"]
},
"fields": ["name", "age"],
"sort": ["-age"],
"page": 1,
"size": 50
}This is equivalent to:
/users?fields=name,age&age=gte:30&age=lte:40&name=like:%Slim%&zipcode=2040&sort=-age
Note: Filter values are always arrays because multiple operators can be applied to the same field (e.g., age has both gte:30 and lte:40).
A single file responsible for starting the HTTP server and running the REST API (e.g., index.ts, app.py, app.go, main.rs).
Located at the top of the import order, the entrypoint:
This is the application's main entry point.
Contains unit tests, integration tests, and other automated tests used to validate the application logic.
/tests naturally sits at the top of the import order alongside /entrypoint, allowing it to import and test all other modules without being imported by them.
While backend architecture focuses on data and business logic, frontend structure emphasizes component reusability and user experience. Despite framework-specific differences, a common organizational pattern emerges across all implementations.
Each framework has its own specifics and terminology, but a common structure can be identified.
Entry files that initialize the application (main) and define the root component (App):
Top-level components that represent entire routes/pages (e.g., /login, /dashboard, /profile)
Reusable UI components and layout building blocks. This convention is shared across all frameworks.
Holds application state management logic (e.g., user session, global UI state, cached data)
Contains general-purpose TypeScript utilities and framework specific logic such as hooks for React and composables for Vue.
Shared type definitions such as Enums, Interfaces, and reusable Types. Centralizes consumed data models and contracts.
Static files such as images, icons, and fonts.
This project uses Tailwind CSS with a custom naming system. The goal is consistency, clarity, and avoiding clashes with Tailwind’s built-in keywords.
surface-* represents the main app background layer. The term surface is preferred over background and bg to avoid naming collisions with Tailwind utilities and base CSS properties.
Variations:
--color-surface: var(--color-white);
--color-surface-alt: var(--color-stone-50);
--color-surface-on: var(--color-stone-100);panel-* represents the complementary surface layer — usually opposite in brightness to the main surface. This allows for clear contrast zones, such as side panels, headers/footers, or sticky overlays. surface/panel is conceptually similar to Bootstrap’s light/dark themes.
Variations:
--color-panel: var(--color-stone-700);
--color-panel-alt: var(--color-stone-600);
--color-panel-on: var(--color-stone-500);pen-* methaphorically represents things written or drawn by a pen such as text, lines and borders. The term avoids collisions with Tailwind utilities like text-* or border-*.
Variations:
--color-pen: var(--color-stone-700);
--color-pen-muted: var(--color-stone-500);
--color-pen-ruler: var(--color-stone-300);
--color-pen-inverse: var(--color-stone-50);These groups follow a similar convention to Bootstrap’s contextual colors. They serve both theming (primary/secondary) and functional roles (success/warning/danger).
--color-primary: var(--color-sky-400);
--color-primary-on: var(--color-sky-600);
--color-primary-surface: var(--color-sky-50);
--color-secondary: var(--color-pink-500);
--color-secondary-on: var(--color-pink-600);
--color-secondary-surface: var(--color-pink-50);
--color-success: var(--color-teal-500);
--color-success-on: var(--color-teal-600);
--color-success-surface: var(--color-teal-50);
--color-warning: var(--color-orange-500);
--color-warning-on: var(--color-orange-600);
--color-warning-surface: var(--color-orange-50);
--color-danger: var(--color-red-500);
--color-danger-on: var(--color-red-600);
--color-danger-surface: var(--color-red-50);The disabled-* group defines styles for inactive or disabled form inputs. It ensures consistency across backgrounds, text, and borders.
Variations:
--color-disabled-surface: var(--color-gray-300);
--color-disabled-pen: var(--color-gray-500);
--color-disabled-ruler: var(--color-gray-300);The backdrop color is used for overlay layers behind modals, dialogs, or drawers. It helps separate focus areas from the rest of the UI.
--color-backdrop: var(--color-stone-300);| Back | FazBrowse Home | New Git URL |