ДокументацияФреймворки контроллера

От NestJS до Некси

Модульный маршрутизатор Nexy сохраняет форму NestJS - контроллеры, модули, внедряемые сервисы и средства защиты - в Python, с Pydantic вместо классов DTO и документацией OpenAPI, созданной для вас.

В каждом разделе ниже показана привычка NestJS, затем то же самое в Nexy. То, что есть в NestJS и чего нет в Nexy, перечислено в Nexy и NestJS.

Включение маршрутизатора

источник/main.ts
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module"; const app = await NestFactory.create(AppModule);await app.listen(3000);

Корневой модуль - это поле nexyconfig.py, а nx dev обслуживает его:

nexyconfig.py
from nexy.core.models import NexyConfigModel from src.apps.app_module import AppModule  class NexyConfig(NexyConfigModel):    useRouter = AppModule

Контроллер

src/cats/cats.controller.ts
import { Controller, Get } from "@nestjs/common"; @Controller("cats")export class CatsController {  @Get()  findAll() {    return [{ name: "Tom" }];  }}

Имя метода - это HTTP-глагол:

src/apps/cats/cats_controller.py
from nexy.decorators import Controller  @Controller("/cats")class CatsController:    async def get(self) -> list[dict]:        return [{"name": "Tom"}]

Контроллер имеет один обработчик для каждого глагола. GET /cats и GET /cats/:id - два контроллера.

Параметры маршрута

src/cats/cat.controller.ts
@Controller("cats/:id")export class CatController {  @Get()  findOne(@Param("id", ParseIntPipe) id: number) {    return { id };  }}

@Param и ParseIntPipe становятся {id} в пути и аннотацией int:

src/apps/cats/cat_controller.py
from nexy.decorators import Controller  @Controller("/cats/{id}")class CatController:    async def get(self, id: int) -> dict:        return {"id": id}

Параметры запроса

src/cats/cats.controller.ts
@Get()findAll(@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number) {  return { limit };}

Параметр запроса - это параметр со значением по умолчанию:

src/apps/search/search_controller.py
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

src/cats/create-cat.dto.ts
import { IsInt, IsString, Min } from "class-validator"; export class CreateCatDto {  @IsString()  name: string;   @IsInt()  @Min(0)  age: number;}
src/cats/cats.controller.ts
@Post()create(@Body() dto: CreateCatDto) {  return dto;}

DTO - это модель Pydantic. Оно всегда проверяется, и плохое тело отвечает 422:

src/apps/cats/cats_dto.py
from pydantic import BaseModel, Field  class CreateCat(BaseModel):    name: str    age: int = Field(ge=0)
src/apps/cats/new_cat_controller.py
from nexy.decorators import Controller from .cats_dto import CreateCat  @Controller("/cats/new")class NewCatController:    async def post(self, body: CreateCat) -> CreateCat:        return body

Провайдеры и внедрение конструктора

src/cats/cats.service.ts
@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__ сообщает контейнеру, что нужно внедрить:

src/apps/cats/cats_service.py
from nexy.decorators import Injectable  @Injectable()class CatsService:    def find_all(self) -> list[dict]:        return [{"name": "Tom"}]
src/apps/cats/cats_controller.py
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()

Области применения

src/cats/request-context.ts
@Injectable({ scope: Scope.REQUEST })export class RequestContext {}
src/apps/cats/request_context.py
from nexy.decorators import Injectable, Scope  @Injectable(scope=Scope.REQUEST)class RequestContext: ...

Scope.SINGLETON используется по умолчанию, как и в NestJS, а Scope.TRANSIENT создает новый экземпляр для каждой инъекции.

Модули

src/cats/cats.module.ts
@Module({  controllers: [CatsController, CatController],  providers: [CatsService],  exports: [CatsService],})export class CatsModule {} @Module({ imports: [CatsModule] })export class AppModule {}

Четыре массива представляют собой четыре атрибута класса:

src/apps/cats/cats_module.py
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]
src/apps/app_module.py
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").

Охранники

src/auth/token.guard.ts
@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 - это функция, которая вызывается, когда запрос не может быть выполнен:

src/apps/admin/admin_controller.py
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": ...}.

Промежуточное программное обеспечение и перехватчики

src/logger.middleware.ts
@Injectable()export class AppHeaderMiddleware implements NestMiddleware {  use(req: Request, res: Response, next: NextFunction) {    res.setHeader("X-App", "demo");    next();  }}

Класс промежуточного программного обеспечения ASGI, указанный в nexyconfig.py, оборачивает каждый запрос:

src/app_header.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 response
nexyconfig.py
from 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. Декораторы лишь добавляют то, о чем не сказано в подписи:

src/apps/docs/documented_controller.py
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 шлюзы

src/events.gateway.ts
@WebSocketGateway()export class EventsGateway {  @SubscribeMessage("echo")  echo(@MessageBody() data: string) {    return data;  }}

Метод SOCKET открывает WebSocket на пути своего контроллера:

src/apps/ws/echo_controller.py
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 или простые переменные среды, внедряемые как любой провайдер:

src/apps/settings.py
import os from nexy.decorators import Injectable  @Injectable()class Settings:    def __init__(self) -> None:        self.database_url = os.getenv("DATABASE_URL", "sqlite:///dev.db")

Тесты

src/cats/cats.controller.spec.ts
const moduleRef = await Test.createTestingModule({ imports: [AppModule] }).compile();const app = moduleRef.createNestApplication();await app.init();await request(app.getHttpServer()).get("/cats").expect(200);
тесты/test_cats.py
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, также может перейти на файловый маршрутизатор, где папка является путем и нет модулей для синхронизации:

src/routes/cats/[id].py
async def GET(id: int):    return {"id": id}

См. Выбор маршрутизатора.

Следующие шаги

Изменить на GitHub

Enter открывает результат, Esc закрывает.