A robust foundation for building APIs using Starlette and SQLAlchemy, featuring a built-in admin interface, OpenAPI documentation, dependency injection, and more.
- Features
- Prerequisites
- Installation
- Configuration
- Running the Application
- Project Structure
- Development
- License
- Support
- Modern API Framework: Built with Starlette for high performance and async support.
- Database Integration: Async SQLAlchemy with ActiveRecord pattern support (provided by SQLActive).
- Admin Interface: Built-in admin panel for data management (provided by Starlette-Admin).
- API Documentation: Automatic OpenAPI/Swagger documentation.
- Authentication: JWT-based authentication system.
- Dependency Injection: Dependency injection for services, request path parameters and bodies via Pydantic models (provided by Starlette DI).
- CORS Support: Configurable CORS middleware.
- Environment Configuration: Easy environment-based configuration.
- Logging: Structured logging with configurable levels.
- OData V4 Query Parameters: Support for OData V4 query parameters provided by OData V4 Query.
- Database Seeding: Sample data generation for development.
- Type Safety: Full type hints support.
- Middleware Stack: Pre-configured middleware for security and functionality.
- Python 3.10+
- A database compatible with SQLAlchemy (e.g., PostgreSQL, SQLite)
- Clone the repository:
git clone <this-repository-url>
cd <repository-name>
- Create and activate a virtual environment:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
- Install dependencies:
pip install -r requirements.txt
- Copy the environment template:
cp .env.example .env
- Configure your
.env
file with appropriate values
Key configuration files:
.env
: Environment variables.logger.conf
: Logging configuration.core/settings.py
: Application settings.
- Initialize and seed the database (development only):
python seed.py
- Start the development server:
python main.py
Let's say you have configured the following values in your .env
file:
HTTP_SCHEMA=http
HOST=localhost
PORT=8000
The application would be available at:
- API: http://localhost:8000
- API Documentation: http://localhost:8000/docs
- Admin Interface: http://localhost:8000/admin
├── core/ # Core functionality
│ ├── api/ # API-related modules
│ ├── auth/ # Authentication and authorization modules
│ ├── bases/ # Base classes
│ ├── definitions.py # Definitions
│ ├── errors.py # Custom exceptions
│ ├── i18n.py # Internationalization module
│ ├── locales.py # Loaded locales
│ ├── logger.py # Application logger (uses the `logger.conf` file)
│ └── settings.py # Configuration
├── dtos/ # Data Transfer Objects (DTOs)
├── lib/ # Libraries and utilities
│ ├── admin_interface/ # Admin interface
│ └── apispec/ # OpenAPI documentation
├── locales/ # Translation files
├── middlewares/ # Middlewares
├── models/ # Database and admin interface models
├── routers/ # API routes
├── services/ # Services (business logic)
├── static/ # Static files
├── templates/ # HTML and Jinja2 templates
├── utils/ # Utility functions and classes
├── .env.example # Environment variables template
├── logger.conf # Logging configuration
├── main.py # Application entry point
└── seed.py # Database seeding script
The entry point of the application is the main.py
file.
The main.py
file is responsible for:
- Configuring the application.
- Initializing the database.
- Mounting the admin interface.
- Mounting the API documentation.
- Mounting the API routes.
- Mounting the static files and templates routes.
- Registering the dependency injection container.
The settings are defined in the core.settings.Settings
class.
It loads the environment variables using the starlette.config.Config
class.
Note
The status
attribute is used to check the status of the server. You can
modify it to change the status of the server if an error occurs. The status
is used by the health
endpoint of routers.health_router.HealthRouter
.
Warning
Don't forget to set the PROD
environment variable to True
when deploying
the application to production. Also, don't forget to set safe values for the
ACCESS_TOKEN_SECRET
and SESSION_SECRET_KEY
environment variables. See the
Environment Variables section for more information.
Dependency injection is used to inject dependencies into services, request path parameters and bodies via Pydantic models.
Visit the Starlette DI repository for more information.
- Create your model in
models/db.py
. - Create admin views in
models/admin.py
.
Create a new DTO in dtos/
inheriting from core.bases.base_dto.BaseDTO
.
There are two types of DTOs: BaseRequestDTO
and BaseResponseDTO
.
The BaseRequestDTO
is used for the body of requests, while the
BaseResponseDTO
is used for the body of responses.
The BaseResponseDTO
has a to_response
method that serializes the DTO to a
dictionary. This method is used to serialize the DTO to a dictionary before
sending it to the client.
Create a new service in services/
inheriting from
core.bases.base_service.BaseService
.
The BaseService
class provides the following methods:
get_odata_count
: Gets the number of items in the query if the$count
option is set toTrue
.get_async_query
: Gets an async query for the given model from the given OData options. Visit the SQLActive repository for more information.to_paginated_response
: Converts a list of items to a paginated response.
Note
If you want to register the service in the dependency injection container,
you should create an interface (abstract class) inheriting from
core.bases.base_service.BaseService
and register it in the
ServiceCollection
instanced in main.py
.
Create a new router in routers/
inheriting from
core.bases.base_router.BaseRouter
.
You don't need to register the router
anywhere. It will be automatically registered as long as it's in the routers/
directory and inherits from core.bases.base_router.BaseRouter
.
You can use the decorators defined in core.api.methods
to create routes like
this:
from core.api.methods import get, post, put, patch, delete
class ExampleRouter(BaseRouter):
@get('/foo')
async def foo(self):
pass
@post('/foo')
async def foo(self, data: RequestDTO):
pass
@put('/foo/{id:str}')
async def foo(self, id: str, data: RequestDTO):
pass
@patch('/foo/{id:str}')
async def foo(self, id: str, data: RequestDTO):
pass
@delete('/foo/{id:str}')
async def foo(self, id: str):
pass
Note that endpoint functions must be async.
You can use the response classes defined in core.api.responses
to return
responses from your endpoints.
You can access the request object via self.request
.
This is done automatically by the custom implementation of the
starlette.routing.Route
class in the core.api.route
module
and the custom implementation of the starlette.requests.Request
class in the
core.api.request
module.
The request object is injected into the router class via dependency injection. It has the following attributes:
id_
: Unique request ID.user
: User session object.service_provider
: Dependency injection service provider.
Authentication and authorization are handled by the core.auth
module.
Use the auth
decorator and the Roles
enum to require authentication and
authorization for an endpoint:
from core.auth.decorator import auth
from core.auth.enums import Roles
class ExampleRouter(BaseRouter):
@auth()
@get('/foo')
async def foo(self):
pass
@auth(Roles.ADMIN) # Requires admin role
@get('/bar')
async def bar(self):
pass
You can also decorate the router itself to require authentication and authorization for all endpoints:
@auth()
class ExampleRouter(BaseRouter):
@get('/foo')
async def foo(self):
pass
@get('/bar')
async def bar(self):
pass
@auth(Roles.ADMIN) # Requires admin role
class ExampleRouter(BaseRouter):
@get('/foo')
async def foo(self):
pass
@get('/bar')
async def bar(self):
pass
OData V4 query parameters are supported. You can use the use_odata
decorator
to indicate that the endpoint uses OData V4 query parameters. Then, use the
parse_odata
method to parse the query parameters:
from core.api.odata import use_odata
class ExampleRouter(BaseRouter):
@use_odata
@get('/foo')
async def foo(self):
odata_options = self.parse_odata() # Parsed OData V4 query
...
Visit the OData V4 Query repository for more information.
Create a new middleware in middlewares/
inheriting from
core.bases.base_middleware.BaseMiddleware
.
Like with routers, you don't need
to register the middleware anywhere. It will be automatically registered as long
as it's in the middlewares/
directory and inherits from
core.bases.base_middleware.BaseMiddleware
.
Create a new translation file in locales/
named as follows: {locale_code}.json
For example: en.json
, es.json
, fr.json
, de.json
, it.json
, etc.
The file must have the following structure:
{
"key": "value",
"key_with_format": "value with {format}",
"nested": {
"key": "value"
}
}
Then you can use the translation in your code like this:
from core import I18N
t = I18N()
t('key')
t('key_with_format', format='value')
t('nested.key')
Note
The I18N
class will automatically load the translation files and provide
the translations, so you don't need to worry about it.
Also, the middlewares.language_middleware.LanguageMiddleware
middleware and
the core.bases.base_router.BaseRouter
class will automatically set the
locale based on the Accept-Language
header of the request. The I18N
class
will use the DEFAULT_LOCALE
setting if the locale is not found.
Note that the I18N
class is registered in the dependency injection container,
so you can inject it in your services and routers.
Create a new base class in core/bases/
.
Create a new exception in core/errors.py
.
Create your utilities in utils/
.
Utility functions and classes should not depend on any other module in the project.
Tip
If the utility is a extended class or a set of classes, you should consider
creating a new package in lib/
instead, so you can convert it to a Python
package in the future if needed.
Create your templates in templates/
and static files in static/
.
Key environment variables:
DATABASE_URL
: Database connection string.PORT
: Server port (default: 8000).PROD
: Production mode flag.ACCESS_TOKEN_SECRET
: JWT secret key.SESSION_SECRET_KEY
: Admin session key.ALLOWED_HOSTS
: Allowed hosts for the server.ALLOW_ORIGINS
: Allowed origins for CORS.ALLOW_METHODS
: Allowed methods for CORS.ALLOW_HEADERS
: Allowed headers for CORS.EXPOSE_HEADERS
: Exposed headers for CORS.
This project is licensed under the MIT License. See the LICENSE file for details.
If you find this project useful, give it a ⭐ on GitHub!