Funktionel programmering i Clojure og backendudvikling på JVM · Lektion

Opbygning af en RESTful API

Konstruér en komplet RESTful API fra bunden med fokus på godkendelse, validering og dataserialisering.

Lektion 1 af 412 trin

Opbygning af en RESTful API er en gratis Funktionel programmering i Clojure og backendudvikling på JVM-lektion på CoddyKit. Dette er lektion 1 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Funktionel programmering i Clojure og backendudvikling på JVM, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Funktionel programmering i Clojure og backendudvikling på JVM-kurset indeholder 4 lektioner i alt.

Hvad er en RESTful API?

En RESTful API (Representational State Transfer Application Programming Interface) er en standardiseret måde, hvorpå computersystemer kan kommunikere over internettet.

Den bruger standardiserede HTTP-metoder (såsom GET, POST, PUT og DELETE) til at udføre handlinger på ressourcer, som er bestemte dele af data eller funktionalitet.

Vigtige principper omfatter:

  • Ressourcer: Alt er en ressource (f.eks. en bruger eller et produkt).
  • Tilstandsløshed: Hver forespørgsel fra en klient til en server skal indeholde alle de oplysninger, der er nødvendige for at forstå forespørgslen.
  • Ensartet grænseflade: En konsekvent måde at interagere med ressourcer på.

Målet med vores Task Manager API

I denne lektion bygger vi en enkel Task Manager API. Denne API giver os mulighed for at:

  • Oprette nye opgaver.
  • Vise alle opgaver.
  • Hente en bestemt opgave.
  • Opdatere en eksisterende opgave.
  • Slette en opgave.

Vi fokuserer på håndtering af JSON-data, grundlæggende godkendelse og validering af input.

Opsætning af vores webserver

Vi bruger Ring til HTTP-abstraktion og Compojure til routing. Her er en grundlæggende opsætning af vores API-server. Vi definerer ruter til vores opgaveressourcer.

Funktionen handler behandler forespørgsler, og run-jetty starter serveren.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})
  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))
  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Fortolkning af JSON-forespørgsler

Når klienter sender data til vores API (f.eks. for at oprette en ny opgave), er dataene ofte i JSON-format. Vi skal fortolke denne JSON-streng til et Clojure-map.

Middleware-komponenten ring.middleware.json/wrap-json-body gør dette for os. Den fortolker forespørgslens body og placerer det resulterende Clojure-map i (:body req).

I eksemplet tilføjede vi (wrap-json-body {:keywords? true}) for automatisk at konvertere JSON-nøgler til Clojure-nøgleord.

Konstruktion af JSON-svar

Vores API skal sende data tilbage til klienter, normalt som JSON. Det indebærer at konvertere Clojure-maps til JSON-strenge og angive den korrekte Content-Type-header.

Middleware-komponenten ring.middleware.json/wrap-json-response håndterer dette. Hvis dit svarfelt :body er et Clojure-map eller en vektor, konverterer den det automatisk til JSON og angiver "Content-Type": "application/json".

Lad os tilføje en GET-rute til en enkelt opgave.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Implementering af grundlæggende godkendelse

Godkendelse bekræfter en klients identitet. Til en simpel API kan vi bruge et token i Authorization-headeren.

Vi opretter en middleware-funktion, der kontrollerer en bestemt API-nøgle. Hvis den mangler eller er ugyldig, returnerer vi status 401 Unauthorized.

Denne middleware omslutter vores vigtigste applikationsruter, så enhver forespørgsel passerer gennem den.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key") ; Example API key

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          new-task (create-task task-data)]
      {:status 201 :body new-task}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate) ; Apply authentication first
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Validering af input til opgaver

Validering sikrer, at de data, der modtages fra klienter, er korrekte og komplette, før de behandles. Det forhindrer fejl og bevarer dataintegriteten.

For vores opgaver sikrer vi, at der altid angives en :title og en :description, når en opgave oprettes eller opdateres.

Hvis valideringen mislykkes, returnerer vi 400 Bad Request med en informativ fejlmeddelelse.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body new-task})
        {:status 400 :body {:message (:error validation)}})))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Opdatering og sletning af ressourcer

For at færdiggøre vores CRUD-operationer (Create, Read, Update, Delete) skal vi bruge PUT- og DELETE-ruter. De arbejder typisk på en bestemt ressource, der identificeres med sit ID.

En PUT-forespørgsel opdaterer en eksisterende opgave, mens en DELETE-forespørgsel fjerner den fra vores tasks-atom.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id)]
    (swap! tasks assoc id new-task)
    new-task))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (vals @tasks)})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body task}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body new-task})
        {:status 400 :body {:message (:error validation)}})))

  (PUT "/tasks/:id" [id req]
    (let [updated-data (:body req)
          validation (validate-task updated-data)]
      (if (:valid validation)
        (if (get @tasks id)
          (do
            (swap! tasks update id merge updated-data)
            {:status 200 :body (get @tasks id)})
          {:status 404 :body {:message "Task not found"}})
        {:status 400 :body {:message (:error validation)}})))

  (DELETE "/tasks/:id" [id]
    (if (get @tasks id)
      (do
        (swap! tasks dissoc id)
        {:status 204 :body nil}) ; 204 No Content for successful deletion
      {:status 404 :body {:message "Task not found"}}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Serialisering af data til output

Nogle gange kan den interne repræsentation af dine data indeholde felter, som du ikke ønsker at eksponere direkte i dine API-svar (f.eks. interne ID'er, adgangskoder og tidsstempler).

Serialisering er processen, hvor interne datastrukturer omdannes til et passende format til API-svaret. Det kan f.eks. være, at vi vil sikre, at et :created-at-tidsstempel har et bestemt strengformat.

Vi kan oprette hjælpefunktioner til at 'rense' eller formatere data, før de sendes som JSON.

(ns coddykit.api
  (:require [compojure.core :refer [defroutes GET POST PUT DELETE]]
            [compojure.route :as route]
            [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.json :refer [wrap-json-response]]
            [ring.middleware.json :refer [wrap-json-body]])
  (:gen-class))

(defonce tasks (atom {}))
(def api-key "my-secret-api-key")

(defn create-task [task-data]
  (let [id (str (java.util.UUID/randomUUID))
        new-task (assoc task-data :id id :created-at (java.time.Instant/now))]
    (swap! tasks assoc id new-task)
    new-task))

(defn format-task-for-api [task]
  (-> task
      (update :created-at str) ; Convert Instant to string
      (dissoc :internal-field) ; Remove any internal fields
      ))

(defn authenticate [handler]
  (fn [request]
    (let [auth-header (get-in request [:headers "authorization"])
          [_ token] (re-matches #"Bearer (.*)" auth-header)]
      (if (= token api-key)
        (handler request)
        {:status 401 :body {:message "Unauthorized"}}))))

(defn validate-task [task]
  (cond
    (nil? (:title task)) {:valid false :error "Title is required"}
    (nil? (:description task)) {:valid false :error "Description is required"}
    :else {:valid true}))

(defroutes app-routes
  (GET "/tasks" [] {:status 200 :body (map format-task-for-api (vals @tasks))})

  (GET "/tasks/:id" [id]
    (if-let [task (get @tasks id)]
      {:status 200 :body (format-task-for-api task)}
      {:status 404 :body {:message "Task not found"}}))

  (POST "/tasks" req
    (let [task-data (:body req)
          validation (validate-task task-data)]
      (if (:valid validation)
        (let [new-task (create-task task-data)]
          {:status 201 :body (format-task-for-api new-task)})
        {:status 400 :body {:message (:error validation)}})))

  (PUT "/tasks/:id" [id req]
    (let [updated-data (:body req)
          validation (validate-task updated-data)]
      (if (:valid validation)
        (if (get @tasks id)
          (do
            (swap! tasks update id merge updated-data)
            {:status 200 :body (format-task-for-api (get @tasks id))})
          {:status 404 :body {:message "Task not found"}})
        {:status 400 :body {:message (:error validation)}})))

  (DELETE "/tasks/:id" [id]
    (if (get @tasks id)
      (do
        (swap! tasks dissoc id)
        {:status 204 :body nil})
      {:status 404 :body {:message "Task not found"}}))

  (route/not-found "Not Found"))

(defn wrap-api-middleware [handler]
  (-> handler
      (authenticate)
      (wrap-json-response)
      (wrap-json-body {:keywords? true :bigdec-enable? true})))

(def app (wrap-api-middleware app-routes))

(defn -main [& args]
  (println "Starting server on port 3000...")
  (run-jetty app {:port 3000 :join? false}))

Håndtering af API-fejl på en hensigtsmæssig måde

En velfungerende API bør altid levere tydelige fejlmeddelelser og passende HTTP-statuskoder, når noget går galt.

Vi har allerede set 401 Unauthorized, 404 Not Found og 400 Bad Request. Ved uventede problemer på serveren er 500 Internal Server Error standarden.

Du kan bruge en global middleware-komponent til fejlhåndtering eller specifikke fejlsvar i dine rutehandlere.

Hurtigt tjek af API-opbygning

Overvej følgende Clojure-handlerfunktion til et API-endpoint:

(defn create-user-handler [request]
  (let [user-data (:body request)
        username (:username user-data)]
    (if (nil? username)
      {:status 400 :body {:message "Username is required"}}
      {:status 201 :body {:message (str "User " username " created!")}})))

Hvis en klient sender en POST-forespørgsel med en tom body (eller en body, der ikke er JSON, og som resulterer i, at (:body request) er nil), hvilken HTTP-statuskode og body returnerer denne handler så, hvis middleware-komponenterne wrap-json-body og wrap-json-response er aktive?

Opsummering: Opbygning af RESTful API'er

I denne lektion har du lært de centrale begreber i forbindelse med opbygning af en RESTful API med Clojure, Ring og Compojure.

  • Vi definerede ressourcer og knyttede HTTP-metoder til handlinger.
  • Du så, hvordan indkommende JSON-forespørgsler fortolkes, og hvordan JSON-svar genereres.
  • Vi implementerede grundlæggende godkendelse ved hjælp af middleware.
  • Du lærte om validering af input for at sikre dataintegriteten.
  • Til sidst berørte vi dataserialisering og effektiv fejlhåndtering.

Dette er grundlæggende byggesten i ethvert robust backend-system!

Gratis at komme i gang

Lær Clojure med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
12
Lektioner
48

Ofte stillede spørgsmål

Er lektionen “Opbygning af en RESTful API” gratis?

Ja — hele teksten til “Opbygning af en RESTful API” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Funktionel programmering i Clojure og backendudvikling på JVM-kurset, skal du opgradere til CoddyKit PRO. Funktionel programmering i Clojure og backendudvikling på JVM-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Opbygning af en RESTful API”?

Konstruér en komplet RESTful API fra bunden med fokus på godkendelse, validering og dataserialisering. Du øver dig i Funktionel programmering i Clojure og backendudvikling på JVM med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Funktionel programmering i Clojure og backendudvikling på JVM?

Der kræves ingen tidligere erfaring. Funktionel programmering i Clojure og backendudvikling på JVM på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 1 af 4.

Hvor lang tid tager lektionen “Opbygning af en RESTful API”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Funktionel programmering i Clojure og backendudvikling på JVM-lektion?

Ja. Alle Funktionel programmering i Clojure og backendudvikling på JVM-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Opbygning af en RESTful API
  2. Hændelsesdrevne arkitekturer
  3. Systemdesign og skaleringsmønstre
  4. Godkendelse og autorisation
← Tilbage til Funktionel programmering i Clojure og backendudvikling på JVM