Pydantic är ett kraftfullt bibliotek som bygger på standardtyper i Python, såsom strängar, heltal och ordböcker, vilket gör det lätt att komma igång för den som redan är bekant med språket. Dess användning sträcker sig dock långt bortom de grundläggande typerna, och biblioteket tillhandahåller ett brett spektrum av anpassade typer och lösningar för att hantera vanliga problem vid datavalidering. I detta avsnitt fokuserar vi på några av de mest användbara funktionerna, inklusive strikta typer, begränsade typer och anpassade fält, för att effektivisera modellering och validering av data.

En av de mest centrala egenskaperna i Pydantic är hanteringen av strikta typer, som StrictBool, StrictInt, StrictStr och andra. Dessa typer säkerställer att värden endast valideras om de exakt matchar den förväntade typen, utan att utföra någon form av typomvandling. Till exempel, om vi specificerar ett StrictInt, måste värdet vara ett heltal och inte en sträng som "1" eller ett flyttal som 1.0. Detta kan vara användbart när man arbetar med värden som strikt måste vara av en viss typ för att uppfylla specifika affärslogikkrav.

Begränsade typer, såsom condate() eller conlist(), tillhandahåller ännu fler anpassningsmöjligheter genom att lägga till extra valideringskrav på befintliga typer. Till exempel kan condate() definiera datumintervall, medan conlist() kan användas för att säkerställa att en lista har ett specifikt antal element, eller att elementen i listan är unika. Dessa verktyg gör det enklare att definiera restriktioner för data och på så sätt undvika vanliga fel i applikationer.

Men Pydantic begränsar sig inte enbart till primitiva datatyper. Biblioteket erbjuder en rad valideringsfunktioner för mer komplexa typer, till exempel för att validera e-postadresser eller för att säkerställa att hela listor av data uppfyller specifika kriterier, som att en lista måste ha ett minimum- eller maximumantal element, eller att värden inom en lista måste vara unika.

För att göra dessa typer ännu mer användbara har Pydantic en Field-klass som kan användas för att anpassa modellens fält. Genom att använda Field kan man tilldela standardvärden, ange alias, och lägga till metadata som gör modellen mer flexibel och lättanvänd. I följande exempel definieras en användarmodell med hjälp av Field för att skapa mer specifika fältdefinitioner:

python
from typing import Literal
from pydantic import BaseModel, Field class UserModelFields(BaseModel): id: int = Field(...) username: str = Field(...) email: str = Field(...) account: Literal["personal", "business"] | None = Field(default=None) nickname: str | None = Field(default=None)

I denna modell är alla fält deklarerade med Field, vilket gör att man kan tilldela mer exakt metadata som kan användas för validering eller dokumentation. Den största fördelen med att använda Field är att det möjliggör enklare anpassningar och gör modellen mer flexibel.

När det kommer till att hantera inkompatibla datakällor, erbjuder Pydantic alias som en lösning för att överbrygga skillnader mellan olika system. Om man till exempel har en datakälla där fältnamnen inte matchar exakt med fältnamnen i modellen, kan man använda alias för att mappa fälten korrekt:

python
class UserModelFields(BaseModel):
id: int = Field(alias="user_id") username: str = Field(alias="name") email: str = Field() account: Literal["personal", "business"] | None = Field(default=None, alias="account_type") nickname: str | None = Field(default=None, alias="nick")

Med dessa alias kan man säkerställa att inkommande data från externa system valideras korrekt mot den interna modellen. Detta gör integrationen av olika system mycket smidigare och effektivare.

En annan viktig aspekt av Pydantic är hur det hanterar numeriska fält. Till exempel kan man specificera att ett heltal ska vara positivt eller negativt, eller att ett värde måste uppfylla ett visst intervall eller vara jämnt. Här är ett exempel på hur man kan modellera ett schackturneringsevent med Pydantic, där flera fält har restriktioner som säkerställer att värdena är korrekta:

python
from datetime import datetime from uuid import uuid4 from pydantic import BaseModel, Field class ChessTournament(BaseModel): id: int = Field(strict=True) dt: datetime = Field(default_factory=datetime.now) name: str = Field(min_length=10, max_length=30)
num_players: int = Field(ge=4, le=16, multiple_of=2)
code:
str = Field(default_factory=uuid4)

Här ser vi en modell som använder flera olika typer av valideringar: datumfältet får sitt värde från en funktion (default_factory), antalet spelare måste vara mellan 4 och 16 och vara ett jämnt tal, och id:t måste vara ett strikt heltal. Detta gör modellen både flexibel och robust mot felaktig data.

Sammanfattningsvis är Pydantic ett kraftfullt verktyg för att skapa och validera datamodeller i Python. Genom att använda Field-klassen och olika anpassade valideringstyper kan man skapa precisa och flexibla modeller som säkerställer att data alltid är korrekt och följer affärslogikens krav. Det är viktigt att förstå att Pydantic inte bara handlar om att definiera datatyper, utan också om att tillämpa strikta regler och anpassningar som gör modellen mer användbar och pålitlig i praktiska tillämpningar.

Hur kan man anpassa och validera data i Pydantic för komplexa API:er?

Pydantic erbjuder ett kraftfullt ramverk för datavalidering och serialisering i Python, vilket gör det till ett ovärderligt verktyg för utvecklare som arbetar med webbutveckling och API:er. För att skapa robusta och flexibla system kan man behöva justera standardbeteendet genom att använda olika tekniker som anpassad serialisering, validering och konfiguration. En grundläggande förståelse för dessa tekniker är nödvändig för att effektivt hantera data i komplexa applikationer.

Ett exempel på hur man kan skapa ett modellobjekt i Pydantic och serialisera det till en Python-dictionary ser ut så här:

python
u = UserModel(id=1, username="freethrow", email="[email protected]", password="password123")
print(u.model_dump())

Resultatet blir en enkel Python-dictionary:

python
{'id': 1, 'username': 'freethrow', 'email': '[email protected]', 'password': 'password123'}

För att omvandla modellen till en JSON-sträng och samtidigt utesluta vissa fält, som till exempel lösenordet, kan man göra så här:

python
print(u.model_dump_json(exclude=set("password")))

Och här är det resulterande JSON-objektet utan lösenordet:

json
{"id":1,"username":"freethrow","email":"[email protected]"}

Detta exempel visar hur man kan använda Pydantic för att skapa och manipulera modeller på ett enkelt sätt. En viktig aspekt av Pydantic är möjligheten att konfigurera fälten i modeller genom det reserverade model_config-fältet. Detta gör det möjligt att ange regler för hur data ska behandlas under deserialisering, och det är särskilt användbart när man arbetar med externa system där man kanske vill tillåta eller förhindra specifika typer av fält.

Ett vanligt användningsområde är när man arbetar med API:er som kommunicerar med olika system. Till exempel kan man använda alias i fält för att översätta fältens namn mellan olika API-standarder. Pydantic erbjuder också stöd för att skapa egna serialiseringsfunktioner via dekoratorer, vilket gör det möjligt att anpassa hur data presenteras både i Python och JSON.

Ett exempel på detta är när man skapar en bankkontomodell och använder anpassad serialisering för att runda av balansvärdet till två decimaler och formatera datumen i ISO-format:

python
from datetime import datetime from pydantic import BaseModel, field_serializer class Account(BaseModel): balance: float updated: datetime @field_serializer("balance", when_used="always")
def serialize_balance(self, value: float) -> float:
return round(value, 2) @field_serializer("updated", when_used="json") def serialize_updated(self, value: datetime) -> str: return value.isoformat()

När modellen används kan man inspektera den serialiserade datan och se hur balansvärdet har rundats av och datumet har formatats enligt ISO-standard:

python
account_data = {
"balance": 123.45545, "updated": datetime.now(), } account = Account.model_validate(account_data) print("Python dictionary:", account.model_dump()) print("JSON:", account.model_dump_json())

Resultatet skulle bli:

python
Python dictionary: {'balance': 123.46, 'updated': datetime.datetime(2024, 5, 2, 21, 34, 11, 917378)}
JSON: {"balance":123.46,"updated":"2024-05-02T21:34:11.917378"}

Pydantic ger också möjlighet att definiera egna validerare för fält, vilket gör det möjligt att säkerställa att data uppfyller specifika regler innan modellen skapas. Dessa validerare definieras med hjälp av dekoratorer och kan användas för att göra mer komplexa kontroller på enskilda fält. Här är ett exempel på en validerare för att säkerställa att titeln på en artikel innehåller en viss sträng:

python
from pydantic import BaseModel, field_validator
class Article(BaseModel): id: int title: str content: str published: bool @field_validator("title") @classmethod def check_title(cls, v: str) -> str: if "FARM stack" not in v: raise ValueError('Title must contain "FARM stack"') return v.title()

Valideraren check_title säkerställer att titeln innehåller texten "FARM stack" och returnerar sedan titeln i titelstil. Om detta krav inte uppfylls, kastas ett fel.

För mer komplexa valideringar, där interaktioner mellan olika fält behöver beaktas, erbjuder Pydantic möjlighet att definiera modellvaliderare. Modellvaliderare kan användas för att kontrollera relationen mellan olika fält och utföra komplexa logiska kontroller innan eller efter att modellen instansieras.

För att illustrera detta kan man tänka sig en användarmodell med två lösenordsfält som måste matcha innan en användare kan registrera sig:

python
from pydantic import BaseModel, EmailStr, ValidationError, model_validator from typing import Any, Self class UserModelV(BaseModel): id: int username: str email: EmailStr password1: str password2: str @model_validator
def check_passwords(cls, values: dict) -> dict:
if values['password1'] != values['password2']: raise ValueError("Passwords must match") return values

Modellvalideraren check_passwords ser till att båda lösenorden matchar, och om de inte gör det, kastar den ett valideringsfel.

Pydantic ger oss många verktyg för att säkerställa dataintegritet och korrekt serialisering av objekt. Det kan hantera både enkla och komplexa scenarier och erbjuder flexibilitet genom användning av dekoratorer, fältalias och modellkonfiguration. Genom att förstå och använda dessa funktioner på rätt sätt kan vi skapa mycket robusta system som effektivt hanterar data och kommunicerar med andra system på ett säkert och korrekt sätt.

Hur implementerar man användarautentisering med JWT i React och FastAPI?

I denna sektion diskuteras implementeringen av ett grundläggande autentisering- och auktoriseringssystem, där vi använder JWT (JSON Web Tokens) för att autentisera användare mellan frontend och backend. Här går vi igenom hur man skapar en enkel användarautentisering i React genom att lagra och hantera JWT i minnet och lokalt lagring, och hur detta integreras med en FastAPI-backend.

Autentisering är en kritisk del av moderna webapplikationer, och valet av hur man hanterar och lagrar autentiseringstoken, såsom JWT, kan påverka både säkerheten och användarupplevelsen. JWT är ett effektivt sätt att hantera autentisering genom att generera och verifiera tokens, vilket gör det möjligt för en användare att logga in och få åtkomst till skyddade rutter.

I FastAPI, kan JWT användas för att skapa ett system där användarna först registreras, loggas in och sedan får en autentiseringstoken som de skickar med sina förfrågningar. Om en användare försöker ändra eller använda en utgången token kommer servern att svara med en 401 Unauthorized-status, vilket betyder att den givna token är ogiltig.

När det gäller frontend-applikationer i React, som är en UI-bibliotek utan starka åsikter om hur autentisering ska implementeras, är det upp till utvecklaren att välja den bästa metoden för att lagra och hantera JWT. I detta fall kommer vi att titta på två vanliga alternativ: att lagra token i minnet (t.ex. genom att använda Reacts useState) och i localStorage (en HTML5-webblagring som gör det möjligt att lagra nyckel-värdepar med ingen automatisk utgång).

Att lagra JWT i localStorage gör det möjligt för användaren att hålla sig inloggad även efter att webbläsaren har stängts och öppnats igen. Men denna metod har sina nackdelar, till exempel att den kan vara känslig för XSS-attacker, där angripare kan få tillgång till lagrad data genom att köra skadlig JavaScript-kod på sidan. Därför rekommenderar många experter att JWT ska lagras i HTTP-only-cookies, som inte kan nås genom JavaScript och därmed är mer skyddade från sådana attacker. Dessa cookies har också den fördelen att de kan hanteras av servern och hållas åtskilda från frontend-koden.

En annan metod som ofta används är den så kallade refresh-token-strategin, där applikationen genererar en huvudtoken vid inloggning och sedan kan skapa nya tokens (refresh tokens) för att hålla användaren inloggad under en längre period utan att behöva logga in om och om igen.

I React kan Context API användas för att hantera den globala autentiseringsstatusen i hela applikationen utan att behöva skicka props mellan komponenter, en teknik som kallas "prop drilling". Med Context API kan du skapa ett globalt tillgängligt state som kan delas mellan olika komponenter utan att behöva passera det genom varje enskild komponent, vilket förenklar både utvecklingen och hanteringen av autentisering.

För att skapa ett funktionellt autentiseringflöde i React, är det nödvändigt att först bygga en frontend-applikation som kan kommunicera med din FastAPI-backend. Du behöver skapa en enkel SPA (Single Page Application) där användarna kan registrera sig, logga in och, om autentiseringen lyckas, visa en lista med alla registrerade användare. För att implementera detta i React, kan du börja med att sätta upp en grundläggande Vite-projektstruktur, installera och konfigurera Tailwind CSS för att förenkla stiliseringen, och sedan skapa de komponenter som behövs för autentisering.

En viktig aspekt att beakta när du lagrar JWT är säkerheten. Även om localStorage kan vara en praktisk lösning, bör du vara medveten om att det inte ger samma nivå av säkerhet som HTTP-only-cookies. För säkerhets skull är det bättre att använda en strategi som en kombination av HTTP-only-cookies och refresh tokens. Detta gör att din applikation kan balansera mellan säkerhet och användarupplevelse, samtidigt som användaren förblir autentiserad utan att behöva logga in hela tiden.

Vid utveckling av autentiseringssystem är det också viktigt att överväga användarens upplevelse och de potentiella riskerna med varje metod. I komplexa applikationer där säkerhet är en högt prioriterad fråga, måste du överväga fler säkerhetsmekanismer, som t.ex. att införa ytterligare lager av autentisering eller att använda mer avancerade metoder för att förhindra attacker som CSRF (Cross-Site Request Forgery) eller session fixering.

Det är också värt att notera att autentisering och auktorisering inte är en "one-size-fits-all" lösning, och beroende på din applikations krav och användningsområde, bör du alltid väga för- och nackdelarna med olika autentiseringsmetoder för att hitta den bästa lösningen för ditt system.

Hur autentisering och skyddade sidor fungerar i Next.js med hjälp av Server Actions och Iron Session

För att implementera autentisering i en Next.js-applikation behöver vi först skapa en mekanism för att hantera användarsessioner och deras autentisering via JWT (Json Web Tokens). När användaren loggar in, sätts en session med användarnamn och motsvarande JWT. Om sessionen inte sätts, förstörs den. En vidarebefordran till sidan /private sker endast när användaren har loggat in och sessionen har ställts in korrekt. I denna sektion beskriver vi hur man skapar en inloggningsformulär som kommer att användas på inloggningssidan och sedan bygga skyddade sidor där endast autentiserade användare kan få åtkomst.

När du har skapat din första Server Action och satt upp sessionen är du redo att bygga en klientkomponent – inloggningsformuläret som kommer att användas på inloggningssidan. För att skapa en sådan komponent öppnar du en ny fil, /src/app/components/LoginForm.js, där du importerar funktionaliteten från login-åtgärden och använder useFormState från React. Denna nya hook gör det möjligt att uppdatera tillstånd baserat på formuläråtgärden, och ger en strängifierad JSON-objekt med eventuella fel som kan uppstå när användaren försöker logga in.

Komponenten LoginForm kommer att vara en klientkomponent, vilket innebär att den kommer att renderas på klienten och måste därför börja med direktivet "use client". Här är exempel på hur du kan bygga och visa användarens inloggningsformulär.

Formuläret ska innehålla fält för användarnamn och lösenord, samt en inloggningsknapp. I ett realistiskt scenario skulle du också kunna visa eventuella felmeddelanden från servern, till exempel när användarnamn eller lösenord inte stämmer.

Efter att ha uppdaterat /src/app/login/page.js och lagt till LoginForm-komponenten kan du testa inloggningen. Om du försöker logga in med ogiltiga referenser, kommer felmeddelandet att visas som en JSON-sträng nedanför formuläret. Om referenserna är giltiga kommer du att bli omdirigerad till sidan /private och kunna se en säker cookie med den krypterade sessionen som finns tillgänglig över hela applikationen.

När du har fått autentisering att fungera genom iron-session och Next.js Server Actions, är nästa steg att skapa skyddade sidor. I detta fall skapar vi en sida där användaren kan lägga till nya bilar i en MongoDB-databas. Denna sida kommer att vara skyddad, vilket betyder att den endast kan nås av autentiserade användare.

För att skapa en sådan skyddad sida behöver du använda sessionens data, inklusive JWT, för att verifiera om användaren har rätt behörighet. Om sessionen är giltig och innehåller ett användarnamn och JWT, får användaren åtkomst till sidan och kan utföra åtgärder som att skapa nya bilar via formulär och serveråtgärder. Om sessionen är ogiltig kommer användaren att omdirigeras till inloggningssidan.

För att skapa en skyddad sida öppnar du /src/app/private/page.js och redigerar filen för att inkludera logik som kontrollerar sessionens giltighet. Om sessionen innehåller en giltig JWT, visas sidan med sessionens data. Om inte, omdirigeras användaren till /login.

Att implementera utloggning kräver att du skapar en åtgärd som förstör sessionen. I filen /src/actions.js läggs en logout-funktion till, som förstör sessionen och omdirigerar användaren till startsidan.

För att ge användaren möjlighet att logga ut från applikationen kan du skapa en enkel form med en knapp som anropar logout-funktionen. Denna knapp placeras i navigationskomponenten så att användaren kan logga ut när de vill. Om användaren är inloggad visas en "Logga ut"-knapp, annars visas en länk för att logga in.

För att skapa en komplett användarupplevelse kan du se till att navigationskomponenten dynamiskt ändras beroende på om användaren är inloggad eller inte. Om sessionen innehåller en JWT visas länken för att logga ut; annars visas inloggningslänken.

Det är också viktigt att förstå att även om det finns andra sätt att skydda sidor i Next.js, som att använda middleware, så erbjuder metoden med sessioner och serveråtgärder en enkel och effektiv lösning för skyddade sidor. Denna metod fungerar bra i fall där du vill begränsa åtkomst till enstaka sidor snarare än att implementera en global lösning.

För att ytterligare stärka säkerheten och användarupplevelsen kan det vara bra att:

  1. Använda HTTPS för att kryptera kommunikation och skydda sessionen.

  2. Implementera en mekanism för att hantera sessionsutgång, så att användaren måste logga in igen efter en viss period av inaktivitet.

  3. Säkerställa att sessionen förstörs korrekt vid utloggning för att förhindra obehörig åtkomst om användaren inte loggar ut ordentligt.

Hur man implementerar autentisering och serveråtgärder i Next.js för att skapa en bilregistreringssida

Du har nu implementerat inloggningsfunktionen fullständigt. Det finns flera faktorer att tänka på, särskilt cookie-livslängden som anges genom maxAge-egenskapen i filen /src/lib.js. Denna tid bör stämma överens med JWT:ns giltighetstid som FastAPI tillhandahåller från backend. Applikationen saknar medvetet en användarregistrering, eftersom tanken är att endast ett fåtal anställda—användare—kan skapas direkt genom API:et. Som en övning kan du skriva en sida för användarregistrering och använda FastAPI:s /users/register-endpoint.

I nästa avsnitt kommer du att slutföra applikationen genom att skapa en privat sida som endast är synlig för autentiserade användare och som kommer att tillåta enbart försäljare att lägga till nya bilar.

För att skapa en ny bilannons skapar du ett formulär för att infoga nya bilar. Eftersom formulärvalidering redan behandlades i Kapitel 8, "Bygga frontend för applikationen", med hjälp av Zod-biblioteket, kommer du inte att använda ett formulärvalideringsbibliotek här. I en verklig applikation skulle ett sådant bibliotek definitivt användas. Du kommer att skapa en ny serveråtgärd för att utföra API-anropet och återigen använda useFormState—samma mönster som du använde för användarinloggning.

Eftersom formuläret för att införa bilar innehåller många fält (och det kan finnas många fler), kommer du att börja med att abstrahera formulärfältet till en separat komponent. Implementeringen av bilannonsen kommer att delas upp i följande steg:

  1. Skapa en ny komponent för fälten i filen /src/components/InputField.js:

js
const InputField = ({ props }) => {
const { name, type } = props; return ( <div> <label>{name}</label> <input type={type} name={name} /> </div> ); }; export default InputField;

Med InputField på plats skapar du CarForm.

  1. Skapa en ny komponent i filen /src/components/CarForm.js och börja med importerna och arrayen av fält som behövs:

js
"use client";
import { createCar } from "@/actions"; import { useFormState } from "react-dom"; import InputField from "./InputField"; const CarForm = () => { let formArray = [ { name: "brand", type: "text" }, { name: "make", type: "text" }, { name: "year", type: "number" }, { name: "price", type: "number" }, { name: "km", type: "number" }, { name: "cm3", type: "number" }, { name: "picture", type: "file" } ]; const [state, formAction] = useFormState(createCar, {}); return ( <form onSubmit={formAction}> {formArray.map((item, index) => (
<InputField key={index} props={item} />
))}
<button type="submit">Save new car</button> </form> ); }; export default CarForm;

Formuläret använder createCar-åtgärden, som du kommer att definiera i actions.js-filen i ett senare steg.

  1. Formuläret måste visas på den privata sidan, så du redigerar filen /src/app/private/page.js:

js
import CarForm from "@/components/CarForm";
import { getSession } from "@/actions"; import { redirect } from "next/navigation"; const page = async () => { const session = await getSession(); if (!session?.jwt) { redirect("/login"); } return ( <div> <h1>Private Page</h1> <CarForm /> </div> ); }; export default page;

Nu har du skapat formuläret och visat det på /private-sidan. Den enda saknade biten är den motsvarande åtgärden, som du kommer att skapa i nästa steg.

  1. Öppna /src/actions.js och lägg till följande åtgärd för att skapa en ny bil:

js
export const createCar = async (state, formData) => { const session = await getSession(); const jwt = session.jwt;
const result = await fetch(`${process.env.API_URL}/cars/`, {
method: "POST", headers: { Authorization: `Bearer ${jwt}` }, body: formData }); const data = await result.json(); if (result.ok) { redirect("/"); } else { return { error: data.detail }; } };

Åtgärden är enkel och det är själva styrkan med serveråtgärder. Det är bara en funktion som kontrollerar sessionen och JWT:n och utför API:ets POST-anrop. Funktionen bör också inkludera en tidigare omdirigering till inloggningssidan i händelse av att JWT inte finns, men på detta sätt låter du useFormState-haken visa eventuella fel från backend.

Nu har du implementerat webbplatsens specifikation—användare kan logga in och lägga till nya bilar. Efter en kort period för revalidering (15–20 sekunder) kommer bilarna att visas på /car-sidan samt på den dedikerade sidan för den nyligen införda bilen.

När det gäller optimering av sökmotorer (SEO), är det viktigt att förstå hur metadata fungerar i Next.js. Det är en funktion som inte bara gör det lättare för crawlers att hitta och indexera statiskt innehåll utan också ger en effektiv väg att sätta sidtitlar och beskrivningar för att förbättra synligheten på nätet.

Det är också viktigt att notera att, även om användarregistrering inte är en del av denna applikation, kan det vara bra att förbereda och implementera denna funktion för en mer skalbar lösning i framtida projekt. Detta kan göras genom att använda samma serveråtgärder och JWT-baserad autentisering, och det skulle öppna upp för användarhantering direkt i frontend.