ДокументацияJavaScript бэкэнды

От экспресса до Nexy

В Nexy экспресс-маршрут становится файлом Python под src/routes/: app.get - это функция GET, req превращается в типизированные параметры, res.json - в возвращаемое значение, а app.use - в поля конфигурации или middleware.py.

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

Маршрут

index.js
import express from "express"; const app = express(); app.get("/api/hello", (req, res) => {  res.json({ message: "Hello" });}); app.listen(3000);

Путь к файлу - это URL-адрес, имя функции - это метод, а возвращаемое значение - это тело JSON:

src/routes/api/hello.py
async def GET():    return {"message": "Hello"}

req.params

index.js
app.get("/api/users/:id", (req, res) => {  const id = Number(req.params.id);  if (Number.isNaN(id)) return res.status(404).end();  res.json({ id });});

:id - это сегмент [id], а аннотация типа выполняет преобразование и 404:

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

Подстановочный знак * - это файл [...rest].py.

req.query

index.js
app.get("/api/search", (req, res) => {  const q = req.query.q ?? "";  const page = Number(req.query.page ?? 1);  res.json({ q, page });});
src/routes/api/search.py
async def GET(q: str = "", page: int = 1):    return {"q": q, "page": page}

?page=abc отвечает 422 с параметром, который не удался, вместо NaN.

req.body и проверка

index.js
import { z } from "zod"; app.use(express.json()); const NewItem = z.object({ name: z.string(), price: z.number().positive() }); app.post("/api/items", (req, res) => {  const parsed = NewItem.safeParse(req.body);  if (!parsed.success) return res.status(422).json(parsed.error);  res.status(201).json(parsed.data);});

Тело анализируется и проверяется на соответствие модели Pydantic перед запуском функции:

src/routes/api/items.py
from pydantic import BaseModel, Field from nexy.http import JSONResponse  class NewItem(BaseModel):    name: str    price: float = Field(gt=0)  async def POST(item: NewItem):    return JSONResponse(item.model_dump(), status_code=201)

express.Router()

маршруты/admin.js
const admin = express.Router(); admin.get("/stats", (req, res) => res.json({ users: 3 })); app.use("/admin", admin);

Папка представляет собой путь монтирования:

src/routes/admin/stats.py
async def GET():    return {"users": 3}

Промежуточное ПО маршрутизации

маршруты/admin.js
function requireAdmin(req, res, next) {  if (req.get("x-role") !== "admin") return res.status(403).json({ error: "Admins only" });  next();} admin.use(requireAdmin);

dependencies.py выполняется перед каждым маршрутом своей папки. Повышение останавливает запрос, как и отсутствие вызова next():

src/routes/admin/dependents.py
from nexy.http import Header, HTTPException  def require_admin(x_role: str = Header("")) -> None:    if x_role != "admin":        raise HTTPException(status_code=403, detail="Admins only")  dependencies = [require_admin]

app.use промежуточное программное обеспечение

index.js
app.use((req, res, next) => {  const start = Date.now();  res.on("finish", () => console.log(req.method, req.url, Date.now() - start));  next();});

middleware.py в корне маршрутов оборачивает каждый запрос; call_next - это next():

src/routes/middleware.py
import time from nexy.http import Request  async def middleware(request: Request, call_next):    start = time.perf_counter()    response = await call_next(request)    response.headers["x-time-ms"] = f"{(time.perf_counter() - start) * 1000:.1f}"    return response

Хосты в стиле cors, compression, helmet

index.js
import compression from "compression";import cors from "cors"; app.use(cors({ origin: "https://example.com" }));app.use(compression());
nexyconfig.py
from nexy.core.models import NexyConfigModel  class NexyConfig(NexyConfigModel):    useCORS = {"allow_origins": ["https://example.com"]}    useGZip = {"minimum_size": 1000}    useTrustedHost = {"allowed_hosts": ["example.com", "*.example.com"]}    useSession = {"secret_key": "change-me"}
index.js
import cookieParser from "cookie-parser"; app.use(cookieParser()); app.post("/api/theme", (req, res) => {  res.cookie("theme", "dark").json({ theme: "dark" });}); app.get("/api/theme", (req, res) => res.json({ theme: req.cookies.theme ?? "light" }));

Файл cookie - это параметр для чтения и метод ответа для записи:

src/routes/api/theme.py
from nexy.http import Cookie, JSONResponse  async def GET(theme: str = Cookie("light")):    return {"theme": theme}  async def POST():    response = JSONResponse({"theme": "dark"})    response.set_cookie("theme", "dark")    return response

express-session

index.js
import session from "express-session"; app.use(session({ secret: "change-me" })); app.post("/api/login", (req, res) => {  req.session.user = req.body.name;  res.json({ user: req.session.user });});

useSession в nexyconfig.py (выше) включает сеансы; request.session - это дикт:

src/routes/api/login.py
from pydantic import BaseModel from nexy.http import Request  class Login(BaseModel):    name: str  async def POST(request: Request, body: Login):    request.session["user"] = body.name    return {"user": body.name}

res.redirect

index.js
app.get("/old", (req, res) => res.redirect(301, "/new"));
src/маршруты/old.py
from nexy.http import RedirectResponse  async def GET():    return RedirectResponse("/new", status_code=301)

Загрузка файлов с помощью multer

index.js
import multer from "multer"; const upload = multer({ limits: { fileSize: 1_000_000 } }); app.post("/api/upload", upload.single("file"), (req, res) => {  res.json({ name: req.file.originalname, size: req.file.size });});
src/routes/api/upload.py
from nexy.http import HTTPException, UploadFile  async def POST(file: UploadFile):    data = await file.read()    if len(data) > 1_000_000:        raise HTTPException(status_code=413, detail="File too large")    return {"name": file.filename, "size": len(data)}

Просмотры с помощью EJS или Pug

index.js
app.set("view engine", "ejs"); app.get("/users", (req, res) => {  res.render("users", { users: [{ name: "Ada" }, { name: "Alan" }] });});
просмотры/users.ejs
<ul>  <% users.forEach((user) => { %>  <li><%= user.name %></li>  <% }) %></ul>

Страница .nexy - это маршрут и просмотр одновременно:

src/маршруты/users.nexy
---users = [{"name": "Ada"}, {"name": "Alan"}]---<ul>  {% for user in users %}  <li>{{ user.name }}</li>  {% endfor %}</ul>

<%= %> экранируется, а {{ }} - нет: экранируйте то, что посетитель набрал с помощью {{ value | e }}.

express.static

index.js
app.use(express.static("public"));

Писать нечего: public/ обслуживается в /.

Обработчик 404 и обработчик ошибок

index.js
app.use((req, res) => res.status(404).send("<h1>Not found</h1>")); app.use((err, req, res, next) => {  console.error(err);  res.status(500).send("<h1>Something broke</h1>");});

Два файла в корне маршрутов. Клиенты API по-прежнему получают JSON, браузеры получают страницу:

источник/маршруты/notfound.nexy
<h1>Not found</h1>
src/маршруты/errors.nexy
<h1>Something broke</h1>

Веб-сокеты с ws

index.js
import { WebSocketServer } from "ws"; const wss = new WebSocketServer({ server, path: "/ws/echo" });wss.on("connection", (socket) => socket.on("message", (data) => socket.send(data)));
src/routes/ws/echo.py
from fastapi import WebSocket  async def SOCKET(websocket: WebSocket):    await websocket.accept()    await websocket.send_text(await websocket.receive_text())

Тесты с помощью Jest и Supertest

index.test.js
import request from "supertest";import { app } from "./app.js"; test("hello", async () => {  const res = await request(app).get("/api/hello");  expect(res.body).toEqual({ message: "Hello" });});
тесты/test_hello.py
from fastapi.testclient import TestClient from nexy.app import app  def test_hello():    assert TestClient(app).get("/api/hello").json() == {"message": "Hello"}

Бег

Экспресс Некси
npm init + npm i express nx new
nodemon index.js nx dev
NODE_ENV=production node index.js nx build, затем nx start
npm test nx test
нет документации по API /docs, список всех .py маршрутов

Сохранение вашего React интерфейса

Если приложение Express обслуживает React SPA, компоненты перемещаются в src/components/ и отображаются как острова на странице .nexy или весь сайт переходит на клиентскую маршрутизацию с помощью useCSR = True. См. Острова и CSR.

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

Изменить на GitHub

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