DocsFrameworks de contrôleurs
De NestJS à Nexy
Le routeur modulaire de Nexy conserve la forme NestJS - contrôleurs, modules, services injectables et gardes - dans Python, avec Pydantic à la place des classes DTO et les documents OpenAPI construits pour vous.
Chaque section ci-dessous montre une habitude NestJS, puis la même chose dans Nexy. Ce que NestJS a et ce que Nexy n'a pas est répertorié dans Nexy et NestJS.
Allumer le routeur
import { NestFactory } from "@nestjs/core";import { AppModule } from "./app.module"; const app = await NestFactory.create(AppModule);await app.listen(3000);Le module racine est un champ de nexyconfig.py, et nx dev le sert :
from nexy.core.models import NexyConfigModel from src.apps.app_module import AppModule class NexyConfig(NexyConfigModel): useRouter = AppModuleUn contrôleur
import { Controller, Get } from "@nestjs/common"; @Controller("cats")export class CatsController { @Get() findAll() { return [{ name: "Tom" }]; }}Le nom de la méthode est le verbe HTTP :
from nexy.decorators import Controller @Controller("/cats")class CatsController: async def get(self) -> list[dict]: return [{"name": "Tom"}]Un contrôleur a un gestionnaire par verbe. GET /cats et GET /cats/:id sont deux contrôleurs.
Paramètres d'itinéraire
@Controller("cats/:id")export class CatController { @Get() findOne(@Param("id", ParseIntPipe) id: number) { return { id }; }}@Param et ParseIntPipe deviennent un {id} dans le chemin et une annotation int :
from nexy.decorators import Controller @Controller("/cats/{id}")class CatController: async def get(self, id: int) -> dict: return {"id": id}Paramètres de requête
@Get()findAll(@Query("limit", new DefaultValuePipe(10), ParseIntPipe) limit: number) { return { limit };}Un paramètre de requête est un paramètre avec une valeur par défaut :
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 et 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;}Le DTO est un modèle Pydantic. C'est toujours validé, et un mauvais corps répond 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 bodyFournisseurs et injection constructeur
@Injectable()export class CatsService { findAll() { return [{ name: "Tom" }]; }} @Controller("cats")export class CatsController { constructor(private readonly cats: CatsService) {} @Get() findAll() { return this.cats.findAll(); }}Les deux mêmes décorateurs ; l'annotation de type de __init__ indique au conteneur ce qu'il faut injecter :
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()Portées
@Injectable({ scope: Scope.REQUEST })export class RequestContext {}from nexy.decorators import Injectable, Scope @Injectable(scope=Scope.REQUEST)class RequestContext: ...Scope.SINGLETON est la valeur par défaut, comme dans NestJS, et Scope.TRANSIENT donne une nouvelle instance par injection.
Modules
@Module({ controllers: [CatsController, CatController], providers: [CatsService], exports: [CatsService],})export class CatsModule {} @Module({ imports: [CatsModule] })export class AppModule {}Les quatre tableaux sont quatre attributs de classe :
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") préfixe chaque contrôleur du module, comme RouterModule ou setGlobalPrefix("api").
Gardes
@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 {}Une garde est une fonction qui se déclenche lorsque la requête ne peut pas continuer :
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}Exceptions
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()
Les deux répondent à {"detail": ...} avec le code d'état, tandis que NestJS répond à {"message": ...}.
Middleware et intercepteurs
@Injectable()export class AppHeaderMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { res.setHeader("X-App", "demo"); next(); }}Une classe middleware ASGI, répertoriée dans nexyconfig.py, encapsule chaque requête :
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]Un intercepteur qui mappe la réponse est le même middleware, avec le code après call_next.
Fanfaronnade
@ApiOperation({ summary: "List cats" })@ApiResponse({ status: 200 })@Get()findAll() {}
Il n'y a pas de SwaggerModule.setup : chaque contrôleur est dans le schéma servi dans /docs. Les décorateurs ajoutent seulement ce que la signature ne dit pas :
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 passerelles
@WebSocketGateway()export class EventsGateway { @SubscribeMessage("echo") echo(@MessageBody() data: string) { return data; }}Une méthode SOCKET ouvre un WebSocket sur le chemin de son contrôleur :
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())Comme tout contrôleur, il répond une fois qu'un module le répertorie dans controllers. La route d'un contrôleur se termine par un slash : connectez-vous à /ws/echo/, puisqu'un WebSocket n'est pas redirigé comme l'est une requête HTTP.
ConfigModule
@Module({ imports: [ConfigModule.forRoot()] })export class AppModule {} constructor(private config: ConfigService) {}this.config.get("DATABASE_URL");
Les paramètres sont un modèle Pydantic ou des variables d'environnement simples, injectées comme n'importe quel fournisseur :
import os from nexy.decorators import Injectable @Injectable()class Settings: def __init__(self) -> None: self.database_url = os.getenv("DATABASE_URL", "sqlite:///dev.db")Essais
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"}]Pour remplacer un service dans un test, dépendez-en via Depends et utilisez le app.dependency_overrides de FastAPI : voir Nexy et NestJS.
Courir
| NestJS | Nexy |
|---|---|
nest new |
nx new, puis choisissez le routeur modulaire |
npm run start:dev |
nx dev |
npm run build && npm run start:prod |
nx build, puis nx start |
npm test |
nx test |
nest generate controller |
un nouveau *_controller.py, ajouté à son module |
Ou des fichiers au lieu de classes
Une application NestJS qui est principalement CRUD peut également être déplacée vers le routeur basé sur des fichiers, où le dossier est le chemin et où il n'y a aucun module à synchroniser :
async def GET(id: int): return {"id": id}Voir Choisir un routeur.