ДокументацияФреймворки контроллера
От NestJS до Некси
Модульный маршрутизатор Nexy сохраняет форму NestJS - контроллеры, модули, внедряемые сервисы и средства защиты - в Python, с Pydantic вместо классов DTO и документацией OpenAPI, созданной для вас.
В каждом разделе ниже показана привычка NestJS, затем то же самое в Nexy. То, что есть в NestJS и чего нет в Nexy, перечислено в Nexy и NestJS.
Включение маршрутизатора
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module"; const app = await NestFactory.create(AppModule);await app.listen(3000);Корневой модуль - это поле nexyconfig.py, а nx dev обслуживает его:
from nexy.core.models import NexyConfigModel from src.apps.app_module import AppModule class NexyConfig(NexyConfigModel): useRouter = AppModuleКонтроллер
import { Controller, Get } from "@nestjs/common"; @Controller("cats")export class CatsController { @Get() findAll() { return [{ name: "Tom" }]; }}Имя метода - это HTTP-глагол:
from nexy.decorators import Controller @Controller("/cats")class CatsController: async def get(self) -> list[dict]: return [{"name": "Tom"}]Контроллер имеет один обработчик для каждого глагола. GET /cats и GET /cats/:id - два контроллера.
Параметры маршрута
@Controller("cats/:id")export class CatController { @Get() findOne(@Param("id", ParseIntPipe) id: number) { return { id }; }}@Param и ParseIntPipe становятся {id} в пути и аннотацией int:
from nexy.decorators import Controller @Controller("/cats/{id}")class CatController: async def get(self, id: int) -> dict: return {"id": id}Параметры запроса
@Get()findAll(@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number) { return { limit };}Параметр запроса - это параметр со значением по умолчанию:
from nexy.decorators import Controller @Controller("/search")class SearchController: async def get(self, q: str = "", limit: int = 10) -> dict: return {"q": q, "limit": limit}DTO и ValidationPipe
import { IsInt, IsString, Min } from "class-validator"; export class CreateCatDto { @IsString() name: string; @IsInt() @Min(0) age: number;}@Post()create(@Body() dto: CreateCatDto) { return dto;}DTO - это модель Pydantic. Оно всегда проверяется, и плохое тело отвечает 422:
from pydantic import BaseModel, Field class CreateCat(BaseModel): name: str age: int = Field(ge=0)from nexy.decorators import Controller from .cats_dto import CreateCat @Controller("/cats/new")class NewCatController: async def post(self, body: CreateCat) -> CreateCat: return bodyПровайдеры и внедрение конструктора
@Injectable()export class CatsService { findAll() { return [{ name: "Tom" }]; }} @Controller("cats")export class CatsController { constructor(private readonly cats: CatsService) {} @Get() findAll() { return this.cats.findAll(); }}Те же два декоратора; аннотация типа __init__ сообщает контейнеру, что нужно внедрить:
from nexy.decorators import Injectable @Injectable()class CatsService: def find_all(self) -> list[dict]: return [{"name": "Tom"}]from nexy.decorators import Controller from .cats_service import CatsService @Controller("/cats")class CatsController: def __init__(self, cats: CatsService) -> None: self.cats = cats async def get(self) -> list[dict]: return self.cats.find_all()Области применения
@Injectable({ scope: Scope.REQUEST })export class RequestContext {}from nexy.decorators import Injectable, Scope @Injectable(scope=Scope.REQUEST)class RequestContext: ...Scope.SINGLETON используется по умолчанию, как и в NestJS, а Scope.TRANSIENT создает новый экземпляр для каждой инъекции.
Модули
@Module({ controllers: [CatsController, CatController], providers: [CatsService], exports: [CatsService],})export class CatsModule {} @Module({ imports: [CatsModule] })export class AppModule {}Четыре массива представляют собой четыре атрибута класса:
from nexy.decorators import Module from .cat_controller import CatControllerfrom .cats_controller import CatsControllerfrom .cats_service import CatsServicefrom .new_cat_controller import NewCatController @Module()class CatsModule: controllers = [CatsController, CatController, NewCatController] providers = [CatsService] exports = [CatsService]from nexy.decorators import Module from .admin.admin_controller import AdminControllerfrom .cats.cats_module import CatsModulefrom .search.search_controller import SearchController @Module()class AppModule: controllers = [SearchController, AdminController] imports = [CatsModule]@Module(prefix="/api") ставится перед каждым контроллером модуля, например RouterModule или setGlobalPrefix("api").
Охранники
@Injectable()export class TokenGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); return request.headers["x-token"] === "secret"; }} @Controller("admin")@UseGuards(TokenGuard)export class AdminController {}Guard - это функция, которая вызывается, когда запрос не может быть выполнен:
from fastapi import Header, HTTPException from nexy.decorators import Controller, UseGuard def require_token(x_token: str = Header("")) -> None: if x_token != "secret": raise HTTPException(status_code=403, detail="Forbidden") @Controller("/admin")@UseGuard(require_token)class AdminController: async def get(self) -> dict: return {"ok": True}Исключения
throw new NotFoundException("No such cat");throw new ForbiddenException();
from fastapi import HTTPException from nexy import forbidden raise HTTPException(status_code=404, detail="No such cat")forbidden()
Оба отвечают {"detail": ...} кодом состояния, а NestJS отвечает {"message": ...}.
Промежуточное программное обеспечение и перехватчики
@Injectable()export class AppHeaderMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { res.setHeader("X-App", "demo"); next(); }}Класс промежуточного программного обеспечения ASGI, указанный в nexyconfig.py, оборачивает каждый запрос:
from starlette.middleware.base import BaseHTTPMiddleware class AppHeaderMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): response = await call_next(request) response.headers["x-app"] = "demo" return responsefrom nexy.core.models import NexyConfigModel from src.app_header import AppHeaderMiddlewarefrom src.apps.app_module import AppModule class NexyConfig(NexyConfigModel): useRouter = AppModule useMiddlewares = [AppHeaderMiddleware]Перехватчик, отображающий ответ, представляет собой то же промежуточное программное обеспечение с кодом после call_next.
Сваггер
@ApiOperation({ summary: "List cats" })@ApiResponse({ status: 200 })@Get()findAll() {}
SwaggerModule.setup нет: каждый контроллер находится в схеме, обслуживаемой в /docs. Декораторы лишь добавляют то, о чем не сказано в подписи:
from nexy.decorators import Controller, UseResponse, UseRoute @Controller("/documented")class DocumentedController: @UseRoute(summary="List cats", tags=["cats"]) @UseResponse(status_code=200) async def get(self) -> list[dict]: return []WebSocket шлюзы
@WebSocketGateway()export class EventsGateway { @SubscribeMessage("echo") echo(@MessageBody() data: string) { return data; }}Метод SOCKET открывает WebSocket на пути своего контроллера:
from fastapi import WebSocket from nexy.decorators import Controller @Controller("/ws/echo")class EchoController: async def SOCKET(self, websocket: WebSocket) -> None: await websocket.accept() await websocket.send_text(await websocket.receive_text())Как и любой контроллер, он отвечает, как только модуль указывает его в controllers. Маршрут контроллера заканчивается косой чертой: подключиться к /ws/echo/, поскольку WebSocket не перенаправляется так, как HTTP-запрос.
ConfigModule
@Module({ imports: [ConfigModule.forRoot()] })export class AppModule {} constructor(private config: ConfigService) {}this.config.get("DATABASE_URL");
Настройки - это модель Pydantic или простые переменные среды, внедряемые как любой провайдер:
import os from nexy.decorators import Injectable @Injectable()class Settings: def __init__(self) -> None: self.database_url = os.getenv("DATABASE_URL", "sqlite:///dev.db")Тесты
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();const app = moduleRef.createNestApplication();await app.init();await request(app.getHttpServer()).get("/cats").expect(200);from fastapi.testclient import TestClient from nexy.app import app def test_cats(): assert TestClient(app).get("/cats").json() == [{"name": "Tom"}]Чтобы заменить сервис в тесте, положитесь на него через Depends и используйте FastAPI из app.dependency_overrides: см. Nexy и NestJS.
Бег
| NestJS | Некси |
|---|---|
nest new |
nx new, затем выберите модульный маршрутизатор |
npm run start:dev |
nx dev |
npm run build && npm run start:prod |
nx build, затем nx start |
npm test |
nx test |
nest generate controller |
новый *_controller.py, добавленный в его модуль |
Или файлы вместо классов
Приложение NestJS, которое в основном представляет собой CRUD, также может перейти на файловый маршрутизатор, где папка является путем и нет модулей для синхронизации:
async def GET(id: int): return {"id": id}См. Выбор маршрутизатора.