Functioneel programmeren met Clojure en backendontwikkeling op de JVM · Les

Een RESTful API bouwen

Bouw vanaf nul een complete RESTful API, inclusief authenticatie, validatie en dataserialisatie.

Les 1 van 412 stappen

Een RESTful API bouwen is een gratis Functioneel programmeren met Clojure en backendontwikkeling op de JVM-les op CoddyKit. Dit is les 1 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Functioneel programmeren met Clojure en backendontwikkeling op de JVM. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Functioneel programmeren met Clojure en backendontwikkeling op de JVM bevat in totaal 4 lessen.

Wat is een RESTful API?

Een RESTful API (Representational State Transfer Application Programming Interface) is een standaardmanier waarop computersystemen via het web met elkaar communiceren.

De API gebruikt standaard-HTTP-methoden (zoals GET, POST, PUT en DELETE) om acties uit te voeren op resources. Dat zijn specifieke stukjes gegevens of functionaliteit.

Belangrijke principes zijn:

  • Resources: alles is een resource (bijvoorbeeld een gebruiker of een product).
  • Statelessness: elk verzoek van een client aan een server moet alle informatie bevatten die nodig is om het verzoek te begrijpen.
  • Eenduidige interface: een consistente manier om met resources te werken.

Doel van onze Task Manager-API

In deze les bouwen we een eenvoudige Task Manager API. Met deze API kunnen we:

  • nieuwe taken aanmaken;
  • alle taken weergeven;
  • een specifieke taak ophalen;
  • een bestaande taak bijwerken;
  • een taak verwijderen.

We richten ons op het verwerken van JSON-gegevens, basisverificatie en invoervalidatie.

Onze webserver instellen

We gebruiken Ring voor de abstractie van HTTP en Compojure voor routering. Dit is een eenvoudige basisopzet voor onze API-server. We definiëren routes voor onze taakresources.

De functie handler verwerkt verzoeken en run-jetty start de server.

(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}))

JSON-verzoeklichamen parseren

Wanneer clients gegevens naar onze API sturen (bijvoorbeeld om een nieuwe taak aan te maken), gebeurt dat vaak in JSON-indeling. We moeten deze JSON-tekenreeks parseren naar een Clojure-map.

De middleware ring.middleware.json/wrap-json-body doet dit voor ons. Deze parseert de inhoud van het verzoek en plaatst de resulterende Clojure-map in (:body req).

In het voorbeeld hebben we (wrap-json-body {:keywords? true}) toegevoegd om JSON-sleutels automatisch om te zetten in Clojure-keywords.

JSON-antwoorden samenstellen

Onze API moet meestal gegevens terugsturen naar clients als JSON. Hiervoor moeten Clojure-maps worden omgezet in JSON-tekenreeksen en moet de juiste Content-Type-header worden ingesteld.

De middleware ring.middleware.json/wrap-json-response handelt dit af. Als de :body van je antwoord een Clojure-map of -vector is, wordt deze automatisch omgezet in JSON en wordt "Content-Type": "application/json" ingesteld.

Laten we een GET-route voor één taak toevoegen.

(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}))

Basisverificatie implementeren

Verificatie controleert de identiteit van een client. Voor een eenvoudige API kunnen we een token in de Authorization-header gebruiken.

We maken een middlewarefunctie die controleert op een specifieke API-sleutel. Als die ontbreekt of ongeldig is, retourneren we de status 401 Unauthorized.

Deze middleware wikkelt onze hoofd-routes van de toepassing in, zodat elk verzoek erdoorheen gaat.

(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}))

Invoer valideren voor taken

Validatie zorgt ervoor dat de gegevens die van clients zijn ontvangen correct en volledig zijn voordat ze worden verwerkt. Dit voorkomt fouten en behoudt de integriteit van de gegevens.

Voor onze taken zorgen we ervoor dat bij het aanmaken of bijwerken van een taak altijd een :title en :description worden opgegeven.

Als de validatie mislukt, retourneren we een 400 Bad Request met een behulpzame foutmelding.

(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}))

Resources bijwerken en verwijderen

Om onze CRUD-bewerkingen (Create, Read, Update, Delete) te voltooien, hebben we PUT- en DELETE-routes nodig. Deze werken meestal op een specifieke resource die aan de hand van de ID wordt geïdentificeerd.

Een PUT-verzoek werkt een bestaande taak bij, terwijl een DELETE-verzoek deze uit ons tasks-atoom verwijdert.

(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}))

Gegevens serialiseren voor uitvoer

Soms bevat de interne weergave van je gegevens velden die je niet rechtstreeks in je API-antwoorden wilt tonen (bijvoorbeeld interne ID's, wachtwoorden of tijdstempels).

Serialisatie is het proces waarbij interne gegevensstructuren worden omgezet naar een geschikte indeling voor het API-antwoord. Zo wil je misschien dat een :created-at-tijdstempel een specifieke tekenreeksindeling heeft.

We kunnen helperfuncties maken om gegevens te 'opschonen' of op te maken voordat ze als JSON worden verzonden.

(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}))

API-fouten netjes afhandelen

Een goed werkende API moet altijd duidelijke foutmeldingen en passende HTTP-statuscodes geven wanneer er iets misgaat.

We hebben al 401 Unauthorized, 404 Not Found en 400 Bad Request gezien. Voor onverwachte serverproblemen is 500 Internal Server Error de standaard.

Je kunt een algemene middleware voor foutafhandeling gebruiken of specifieke foutantwoorden binnen je routehandlers retourneren.

Korte controle: API bouwen

Bekijk de volgende Clojure-handlerfunctie voor een API-eindpunt:

(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!")}})))

Als een client een POST-verzoek met een lege body verstuurt (of met een niet-JSON-body die ervoor zorgt dat (:body request) nil is), welke HTTP-statuscode en body retourneert deze handler dan, ervan uitgaande dat de middleware wrap-json-body en wrap-json-response actief zijn?

Samenvatting: RESTful API's bouwen

In deze les heb je de basisconcepten geleerd voor het bouwen van een RESTful API met Clojure, Ring en Compojure.

  • We hebben resources gedefinieerd en HTTP-methoden aan acties gekoppeld.
  • Je hebt gezien hoe je binnenkomende JSON-verzoeken parseert en JSON-antwoorden genereert.
  • We hebben basis-verificatie geïmplementeerd met middleware.
  • Je hebt geleerd over invoervalidatie om de integriteit van gegevens te waarborgen.
  • Tot slot hebben we gegevensserialisatie en effectieve foutafhandeling behandeld.

Dit zijn fundamentele bouwstenen voor elk robuust backendsysteem!

Gratis beginnen

Leer Clojure met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
12
Lessen
48

Veelgestelde vragen

Is de les “Een RESTful API bouwen” gratis?

Ja — de volledige tekst van “Een RESTful API bouwen” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Functioneel programmeren met Clojure en backendontwikkeling op de JVM wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Functioneel programmeren met Clojure en backendontwikkeling op de JVM bevat in totaal 4 lessen.

Wat leer ik in “Een RESTful API bouwen”?

Bouw vanaf nul een complete RESTful API, inclusief authenticatie, validatie en dataserialisatie. Je oefent met Functioneel programmeren met Clojure en backendontwikkeling op de JVM door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Functioneel programmeren met Clojure en backendontwikkeling op de JVM te beginnen?

Ervaring vooraf is niet nodig. Functioneel programmeren met Clojure en backendontwikkeling op de JVM op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 1 van 4.

Hoe lang duurt de les “Een RESTful API bouwen”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Functioneel programmeren met Clojure en backendontwikkeling op de JVM?

Ja. Elke les over Functioneel programmeren met Clojure en backendontwikkeling op de JVM bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Een RESTful API bouwen
  2. Eventgestuurde architecturen
  3. Systeemontwerp en schaalbaarheidspatronen
  4. Authenticatie en autorisatie
← Terug naar Functioneel programmeren met Clojure en backendontwikkeling op de JVM