Bygge et RESTful API
Bygg et komplett RESTful API fra grunnen av, med autentisering, validering og dataserialisering.
Bygge et RESTful API er en gratis leksjon i Clojure: funksjonell programmering og backendutvikling på JVM på CoddyKit. Dette er leksjon 1 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Clojure: funksjonell programmering og backendutvikling på JVM, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Clojure: funksjonell programmering og backendutvikling på JVM inneholder totalt 4 leksjoner.
Hva er et RESTful API?
Et RESTful API (Representational State Transfer Application Programming Interface) er en standard måte datasystemer kommuniserer med hverandre over nettet på.
Det bruker standardiserte HTTP-metoder (som GET, POST, PUT og DELETE) til å utføre handlinger på ressurser, som er bestemte deler av data eller funksjonalitet.
Viktige prinsipper omfatter:
- Ressurser: Alt er en ressurs (for eksempel en bruker eller et produkt).
- Tilstandsløshet: Hver forespørsel fra en klient til en server må inneholde all informasjon som trengs for å forstå forespørselen.
- Enhetlig grensesnitt: En konsekvent måte å samhandle med ressurser på.
Målet med Task Manager-API-et vårt
I denne leksjonen skal vi bygge et enkelt Task Manager API. Dette API-et skal gjøre det mulig å:
- Opprette nye oppgaver.
- Vise alle oppgaver.
- Hente en bestemt oppgave.
- Oppdatere en eksisterende oppgave.
- Slette en oppgave.
Vi fokuserer på håndtering av JSON-data, enkel autentisering og validering av inndata.
Sette opp webserveren vår
Vi skal bruke Ring som HTTP-abstraksjon og Compojure til ruting. Her er et grunnleggende oppsett for API-serveren vår. Vi definerer ruter for oppgaveressursene våre.
Funksjonen handler behandler forespørsler, 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}))Analysere JSON-forespørselskropper
Når klienter sender data til API-et vårt (for eksempel når en ny oppgave opprettes), er dataene ofte i JSON-format. Vi må analysere denne JSON-strengen og gjøre den om til et Clojure-kart.
Middleware-komponenten ring.middleware.json/wrap-json-body gjør dette for oss. Den analyserer forespørselskroppen og legger det resulterende Clojure-kartet i (:body req).
I eksempelet la vi til (wrap-json-body {:keywords? true}) for automatisk å konvertere JSON-nøkler til Clojure-nøkkelord.
Konstruere JSON-svar
API-et vårt må sende data tilbake til klientene, vanligvis som JSON. Dette innebærer å konvertere Clojure-kart til JSON-strenger og angi riktig Content-Type-hode.
Middleware-komponenten ring.middleware.json/wrap-json-response håndterer dette. Hvis :body i svaret er et Clojure-kart eller en vektor, konverterer den det automatisk til JSON og angir "Content-Type": "application/json".
La oss legge til en GET-rute for én oppgave.
(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}))Implementere enkel autentisering
Autentisering bekrefter identiteten til en klient. For et enkelt API kan vi bruke et token i Authorization-hodet.
Vi oppretter en middleware-funksjon som kontrollerer en bestemt API-nøkkel. Hvis den mangler eller er ugyldig, returnerer vi statusen 401 Unauthorized.
Denne middleware-komponenten omslutter hovedrutene i applikasjonen, slik at hver forespørsel går gjennom 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}))Validere inndata for oppgaver
Validering sikrer at dataene som mottas fra klienter, er riktige og fullstendige før de behandles. Dette forhindrer feil og opprettholder dataintegriteten.
For oppgavene våre sørger vi for at både :title og :description alltid oppgis når en oppgave opprettes eller oppdateres.
Hvis valideringen mislykkes, returnerer vi 400 Bad Request med en nyttig feilmelding.
(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}))Oppdatere og slette ressurser
For å fullføre CRUD-operasjonene våre (Create, Read, Update, Delete) trenger vi PUT- og DELETE-ruter. Disse opererer vanligvis på en bestemt ressurs som identifiseres med ID-en sin.
En PUT-forespørsel oppdaterer en eksisterende oppgave, mens en DELETE-forespørsel fjerner den fra tasks-atomet vårt.
(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 av data for utdata
Noen ganger kan den interne representasjonen av dataene inneholde felt De ikke ønsker å eksponere direkte i API-svarene (for eksempel interne ID-er, passord eller tidsstempler).
Serialisering er prosessen med å omforme interne datastrukturer til et egnet format for API-svaret. Det kan for eksempel være ønskelig å sikre at tidsstempelet :created-at har et bestemt strengformat.
Vi kan opprette hjelpefunksjoner for å «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åndtere API-feil på en god måte
Et API som oppfører seg godt, bør alltid gi tydelige feilmeldinger og passende HTTP-statuskoder når noe går galt.
Vi har allerede sett 401 Unauthorized, 404 Not Found og 400 Bad Request. Ved uventede problemer på serveren er 500 Internal Server Error standarden.
De kan bruke en global middleware-komponent for feilhåndtering eller bestemte feilsvar i rutehåndtererne.
Hurtigsjekk: Bygge API-er
Se på følgende Clojure-håndtererfunksjon for et API-endepunkt:
(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ørsel med en tom kropp (eller en kropp som ikke er JSON, slik at (:body request) blir nil), hvilken HTTP-statuskode og kropp returneres av denne håndtereren, forutsatt at middleware-komponentene wrap-json-body og wrap-json-response er aktive?
Oppsummering: Bygge RESTful API-er
I denne leksjonen har De lært de grunnleggende konseptene for å bygge et RESTful API med Clojure, Ring og Compojure.
- Vi definerte ressurser og knyttet HTTP-metoder til handlinger.
- De så hvordan innkommende JSON-forespørsler analyseres og JSON-svar genereres.
- Vi implementerte enkel autentisering ved hjelp av middleware.
- De lærte om validering av inndata for å sikre dataintegriteten.
- Til slutt var vi innom dataserialisering og effektiv feilhåndtering.
Dette er grunnleggende byggesteiner for ethvert robust backend-system!
Lær deg Clojure med en AI-veileder – gratis
Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.
- Kurs
- 12
- Leksjoner
- 48
Ofte stilte spørsmål
Er leksjonen «Bygge et RESTful API» gratis?
Ja – hele teksten i «Bygge et RESTful API» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Clojure: funksjonell programmering og backendutvikling på JVM-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Clojure: funksjonell programmering og backendutvikling på JVM inneholder totalt 4 leksjoner.
Hva lærer jeg i «Bygge et RESTful API»?
Bygg et komplett RESTful API fra grunnen av, med autentisering, validering og dataserialisering. Du øver på Clojure: funksjonell programmering og backendutvikling på JVM med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.
Trenger jeg erfaring for å begynne med Clojure: funksjonell programmering og backendutvikling på JVM?
Ingen tidligere erfaring er nødvendig. Clojure: funksjonell programmering og backendutvikling på JVM på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 1 av 4.
Hvor lang tid tar leksjonen «Bygge et RESTful API»?
De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.
Kan jeg skrive og kjøre kode i denne Clojure: funksjonell programmering og backendutvikling på JVM-leksjonen?
Ja. Alle Clojure: funksjonell programmering og backendutvikling på JVM-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.
Alle leksjonene i dette kurset
- Bygge et RESTful API
- Hendelsesdrevne arkitekturer
- Systemutforming og skalerbarhetsmønstre
- Autentisering og autorisering