API z bazą danych w kilka minut? – poznaj Supabase (1/2)

Może jesteś developerem, który ma dość korzystania z serwisów typu MockAPI i poszukujesz narzędzia, które pozwoli stworzyć API z mającymi sens danymi dla wersji poglądowej Twojej aplikacji w kilka chwil?

A może jesteś początkującym programistą front-end, który chciałby, żeby jego projekty w portfolio wyróżniały się, a aplikacja TODO napisana w React’cie zaczęła przechowywać dane w innych miejscach, niż tylko w wewnętrznym stanie?

W obu tych przypadkach potrzebny jest Ci prosty (i najlepiej darmowy) sposób, aby nawet bez znajomości technologii back-endowych, stworzyć zaplecze dla Twojego niesamowitego projektu!

API – czyli interfejs programowania aplikacji, to nic innego jak zestaw reguł opisujący, w jaki sposób poszczególne aplikacje (lub części naszego systemu) komunikują się ze sobą.

Czym jest Supabase?

Supabase, reklamuje się jako open-source’owa alternatywa dla popularnego serwisu Firebase, o którym już mogłeś kiedyś usłyszeć. W istocie usługi oferowane przez oba te serwisy nie są identyczne, jednak dla Nas przede wszystkim liczy się to, co Supabase może dla Nas zrobić (a może naprawdę dużo!). Supabase to serwis udostępniający bazę danych Postgres, wraz z gotowym uwierzytelnianiem, przechowywaniem plików oraz automatycznie generowanym API. Dokumentacja dostępna pod tym linkiem Supabase Docs jest naprawdę obszerna i bardzo przystępnie napisana, dlatego zachęcam Cię do zajrzenia do niej i zobaczenia, co jeszcze można zrobić z wykorzystaniem tego narzędzia. Ponieważ wspomniana już wcześniej aplikacja TODO jest najczęściej wybierana, jako projekt startowy dla wielu przyszłych programistów, to właśnie na jej podstawie, przedstawię w tym artykule, w jaki sposób stworzyć bazę danych i zintegrować Supabase z aplikacją napisaną w React’cie.
Krok 1

Tworzenie projektu w Supabase

Aby rozpocząć pracę z Supabase, należy utworzyć konto. Na szczęście twórcy postawili na wygodę, dlatego bez problemu możesz zarejestrować się z użyciem swojego konta na Githubie.
Jeżeli nie masz tam konta, to zachęcam Cię do utworzenia go. Github, to podstawowe narzędzie, które pozwoli przyszłemu pracodawcy zobaczyć projekty, które tworzyłeś. Poza tym, znajomość git’a, to must have w większości firm, dlatego tym bardziej warto rozpocząć swoją przygodę z nim jak najwcześniej.

W Supabase masz możliwość tworzenia organizacji, a następnie zakładania w nich projektów. Utwórz nową organizację o dowolnej nazwie – w moim przypadku będzie to Dogtronic 🙂 – i przejdź do wybrania jej ustawień.

Wybierz z listy serwer znajdujący się najbliżej Twojej lokacji i podaj nazwę projektu oraz hasło dla swojej bazy danych. Hasło to powinno być silne i przechowywane w bezpiecznym miejscu, aby uniemożliwić innym dostęp do Twojej bazy.

Po zakończeniu konfiguracji, zobaczysz panel główny swojego projektu. Znajdziesz tu klucz publiczny dla swojego API, klucz prywatny, a także listę przykładowych projektów w wielu frameworkach i językach programowania.

Klucz prywatny (oznaczony service_role, secret) pozwala na dokonywanie dowolnych zmian w bazie danych, włącznie z usunięciem całej jej zawartości. Nie udostępniaj go nikomu!
Krok 2

Tworzenie bazy danych

W wyświetlonym panelu, pierwszy z pokazanych kafelków powinien być zatytułowany Database. Z tego miejsca możesz utworzyć nową bazę z użyciem edytora graficznego (zalecany dla początkujących) lub z edytora SQL, gdzie będziesz mógł wykonywać zapytania oraz tworzyć tabelę z użyciem języka SQL.

Masz tutaj także link do dokumentacji, do której warto wracać w momentach zacięcia się podczas pracy. Dla ułatwienia w naszym przypadku wykorzystamy edytor graficzny Table Editor.

Przy okazji, zastanówmy się, jak powinna wyglądać baza danych dla naszego projektu. Pierwszą i najważniejszą z tabel powinniśmy przeznaczyć na nasze zadania na liście. Chcielibyśmy zapewne również, aby użytkownicy mogli przy rejestracji podać swoje podstawowe dane (imię, nazwisko, może pseudonim?).

Dodatkową funkcjonalnością może być także kategoria zadania, którą użytkownik będzie mógł zdefiniować samodzielnie, aby następnie móc pogrupować swoje zadania. Zobaczmy zatem, jak taka baza mogłaby wyglądać na schemacie ERD.

W Supabase, tabela użytkowników, którzy zarejestrują się w naszej aplikacji (nazwana users) jest generowana automatycznie. Aby dodać jednak dodatkowe pola, które mogą być potrzebne w naszej aplikacji, najlepiej jest utworzyć dodatkową tabelę, a następnie powiązać ją relacją z tabelą domyślną.
Schemat ERD utworzonej bazy danych

Przy tworzeniu tabel zostanie pokazana opcja „Enable Row Level Security (RLS)”. Na ten moment proponuję pozostawić ją wyłączoną, ponieważ przygotowaniem polityk Postgres i  zabezpieczeń zajmiemy się innym razem.

Widok tworzenia tabeli
Widok dodawania klucza obcego
Widok dodawania klucza obcego
Po utworzeniu tabel, możemy sprawdzić, czy nasza baza zgadza się z przygotowanym diagramem. Można do tego wykorzystać darmowe narzędzie Supabase Schema utworzone przez @zernonia. Po podaniu linku do naszego API oraz klucza publicznego powinien pojawić się przed Tobą następujący schemat:
Schemat bazy danych wygenerowany przez Supabase Schema
Krok 3

Kodowanie

Nareszcie możemy przejść do części, na którą czekali wszyscy (a przynajmniej ja), czyli pisania kodu!

Nie tracąc czasu, otwórz swój ulubiony edytor kodu i zaczynamy!

Teraz możesz wybrać jedną z dwóch ścieżek: podstawową lub zaawansowaną

Ścieżka podstawowa

Jeżeli chcesz na potrzeby nauki korzystania z Supabase dodatkowo napisać prostą aplikację Todo, przejdź dalej do ścieżki zaawansowanej.

Jeśli jednak nie czujesz takiej potrzeby, gotową aplikację znajdziesz tutaj.

Ścieżka zaawansowana

Jeżeli jednak masz trochę więcej czasu, to zachęcam Cię do samodzielnego pisania kodu, a następnie przejścia do sekcji integracji z Supabase.

Omówmy zatem, co znajduje się w przykładowej aplikacji Todo. Składa się ona z następujących komponentów:

  • Todo
  • TodoList
  • App
  • Login

Póki co ich funkcja jest czysto prezentacyjna, dlatego nie ma w nich nic specjalnego.

Ważniejszy jest natomiast plik supabaseClient.ts. To on pozwoli nam na połączenie się z naszym API poprzez utworzony obiekt klienta. W katalogu głównym możesz zauważyć również plik .env, w którym przechowywany będzie URL do naszego API, a także klucz publiczny.

Zanim zaczniemy, z panelu ustawień Supabase (ikona koła zębatego po lewej stronie) wybierz zakładkę API i skopiuj swój adres URL i klucz publiczny.

import { createClient } from '@supabase/supabase-js'

 const supabaseUrl:string = process.env.REACT_APP_SUPABASE_URL!
 const supabaseAnonKey:string = process.env.REACT_APP_SUPABASE_ANON_KEY!

 export const supabase = createClient(supabaseUrl, supabaseAnonKey)
Panel ustawień Supabase - API
Panel ustawień Supabase - API
Następnie umieść w odpowiednich miejscach skopiowane dane w pliku .env. Możesz teraz uruchomić projekt wywołując:
npm start
Krok 4

Logowanie

Na początku zajmijmy się panelem logowania. W pliku Login.tsx mamy wszystko, co będzie nam potrzebne, aby zalogować się do aplikacji za pomocą „magicznego linku”, czyli po prostu łącza, które wysłane zostanie na wskazany przez nas adres email w panelu logowania.

Dodajmy zatem obsługę wysłania formularza. Wewnątrz funkcji handleLogin zastąpmy wywołanie alerta tak, aby uzyskać:

const handleLogin = async (e) => {  
    e.preventDefault()  
  
    try {  
        setLoading(true)  
        const { error } = await supabase.auth.signIn({ email })  
        if (error) throw error  
            alert('Check your email for the login link!')  
    } catch (error:any) {  
        alert(error.error_description || error.message)  
    } finally {  
        setLoading(false)  
    }  
}
  1. Jeżeli klikniesz teraz na przycisk, zobaczysz inny komunikat.
LocalhostP:3000 - Komunikat 1

2. Wprowadź swój adres email i kliknij ponownie przycisk, po chwili powinieneś zobaczyć następujący alert:

LocalhostP:3000 - Komunikat 1
LocalhostP:3000 - Komunikat 2

Jeżeli sprawdzisz swoją skrzynkę pocztową, powinieneś zobaczyć maila z prośbą o potwierdzenie. Po kliknięciu w link zostaniesz przekierowany na swoję stronę. Możesz także sprawdzić, w zakładce Authentication w panelu Supabase, że twój mail pojawił się w tabeli. Oznacza to, że zostałeś zarejestrowany!

Mamy już działającą możliwość zalogowania się do naszej aplikacji, jednakże po przekierowaniu z magicznego linku nic się nie dzieje i znów widzimy panel logowania. Musimy zatem dodać logikę, która pozwoli nam sprawdzić, czy użytkownik wchodzący na stronę jest zalogowany.

Supabase posługuje się JWT (JSON Web Token) w celu uwierzytelnienia użytkownika. Token ten jest dodawany do każdego zapytania wysłanego do API i pozwala sprawdzić po stronie serwera, czy osoba ma dostęp do oczekiwanego zasobu.

 

Jeżeli chcesz zobaczyć, jak wygląda wygenerowany token, w narzędziach deweloperskich swojej przeglądarki wejdź w zakładkę Aplikacja > Pamięć lokalna > localhost i znajdź rekord supabase.auth.token.

W pliku App.tsx potrzebujemy stanu, który pozwoli nam przechowywać aktualną sesję, a także funkcji, która pozwoli zaktualizować ten stan przy pierwszym uruchomieniu aplikacji. Skorzystamy zatem z dwóch podstawowych hook’ów oferowanych przez React’a, czyli useState oraz useEffect. Dodajmy zatem następujący kod:

const [session, setSession] = useState<Session | null>(null)

 useEffect(() => {
    setSession(supabase.auth.session())
    supabase.auth.onAuthStateChange((_event, session) => {
        setSession(session)
    });
 }, []);

W ten sposób, podczas pierwszego renderowania naszej aplikacji zapisana zostanie aktualna sesja. o ile użytkownik jest zalogowany. Następnie, wykorzystując dobrodziejstwo Supabase, w prosty sposób dodajemy listenner, który uważać będzie na zmiany stanu autoryzacji.

Dzięki temu, w przypadku np. wygaśnięcia tokenu, automatycznie zaktualizowana zostanie zmienna stanu przechowująca sesję.

Ostatnim krokiem jest dodanie logiki, która w zależności od tego, czy użytkownik jest zalogowany, pokaże mu panel logowania lub listę jego zadań. Wewnątrz metody render dodajemy zatem:

{!session ? <Login /> : <TodoList/>}
Jeżeli spojrzysz teraz na aplikację, zobaczysz, że zostaliśmy przeniesieni do listy zadań. Jest to znak, że konfiguracja przebiegła prawidłowo.  Jeżeli w którymś miejscu zgubiłeś się, możesz sprawdzić gotowy kod dla tego etapu na Githubie.
Krok 5

Wyświetlanie zadań

Możemy się już zalogować, ale póki co widzimy po prostu szablonowe zadania, które nie do końca nas przekonują. Dodajmy zatem teraz możliwość oglądania własnych zadań.

Jeżeli w panelu Supabase przejdziesz do zakładki API, zobaczysz coś, co jest jedną z największych zalet oferowanych przez ten serwis – automatyczna dokumentacja API! To właśnie dzięki temu wdrożenie Supabase do swojego projektu jest szybkie i bezbolesne, nawet bez zaawansowanej znajomości JavaScript’a.

Wykorzystajmy zatem to dobrodziejstwo i przygotujmy funkcję, która pobierze listę naszych zadań, a następnie wyświetli je w formie komponentów Task. Nasza lista powinna być pobierana przy pierwszym renderowaniu listy zadań, dlatego w komponencie TaskList dodajmy hook useEfect.

W jego wnętrzu umieśćmy kod funkcji pozwalającej na pobieranie listy zadań, który znajdziesz w zakładce API przechodząc do Tables and Views > Todo. Kod ten powinien wyglądać następująco:

Zwróćmy jednak uwagę, że powyższe polecenie pozwala na pobranie wszystkich zadań, niezależnie od użytkownika. Aby temu zaradzić, wykorzystajmy jedną z wbudowanych metod-filtrów Supabase – eq.

Pozwala ona na wskazanie w zapytaniu jednej z kolumn naszej tabeli oraz wartości, którą powinny posiadać pobierane rekordy w danej kolumnie. Jeżeli miałeś już styczność z językiem SQL, to możesz spojrzeć na tę metodę, jak na klauzulę WHERE w zapytaniu do bazy danych z użyciem SELECT. 

W naszym przypadku chcemy, aby użytkownik miał możliwość zobaczenia wyłącznie swoich własnych zadań, a na to właśnie wskazuje kolumna user_id, przechowująca id użytkownika dodającego zadanie. Nasza funkcja powinna mieć zatem postać:

let { data: Todo, error } = await supabase
    .from('Todo')
    .select('*')
Więcej o wbudowanych metodach Supabase pozwalających na wykonywanie bardziej skomplikowanych zapytań możesz przeczytać w dokumentacji: Używanie filtrów
const { data: Todos, error } = await supabase
    .from('Todo')
    .select('*')
    .eq('user_id', userId)

Możesz teraz pomyśleć „no dobrze, ale skąd będziemy wiedzieć, jakie jest id zalogowanego użytkownika?”. Nie trzymając Cię w niepewności już spieszę z odpowiedzią. 

Tutaj również mamy już gotowe rozwiązanie dla tego problemu. Supabase oferuje metodę pozwalającą na pobranie obiektu zalogowanego użytkownika ze wszystkimi jego danymi, w tym z jego id. Metoda ta ma postać supabase.auth.user() i zwraca obiekt klasy User, którego część z atrybutów możesz sprawdzić poniżej:

export interface User {

    id: string
    app_metadata: {
        provider?: string
        [key: string]: any
    }
    user_metadata: {
        [key: string]: any
    }
    
    /*...*/
}

Po dodaniu pobierania id użytkownika nasza funkcja ma póki co postać:

const userId = supabase.auth.user()!.id

 const { data: Todos, error } = await supabase
    .from('Todo')
    .select('*')
    .eq('user_id', userId)

Znak „!” na końcu metody user() to post-fixowy operator Non-null assertion (nie będę nawet próbował przetłumaczyć tego na polski, żeby bardziej nie kaleczyć języka :P).

 

Wskazuje on kompilatorowi TypeScript, że jego operand ma wartość inną niż null i undefined. Jeżeli piszesz w JavaScripcie, nie dodawaj go tam. Po kompilacji z TS do JS jest on usuwany, więc jest on potrzebny jedynie w przypadku programowania w TS.

Wszystko wygląda świetnie! Teraz pozostało jedynie opakować nasz kod w blok try-catch oraz dodać zapisywanie pobranych danych do wewnętrznego stanu naszej aplikacji. Dodaj dwa stany – todos (typu ITodo[] – plik models.ts) oraz loading (typ boolean), a następnie dodaj odpowiednio te funkcjonalności w następujący sposób:

try {
    setLoading(true);
    const userId = supabase.auth.user()!.id;
    const { data: Todos, error, status } = await supabase
        .from('Todo')
        .select('*')
        .eq('user_id', userId);

    if (error && status !== 406) {
        throw error;
    }
    
    if (Todos) {
        setTodos(Todos);
    }

 }
 catch (error:any) {
    alert(error.message);
 }
 finally {
    setLoading(false);
 }

Po umieszczeniu powyższego kodu w funkcji (asynchronicznej!) a następnie wywołaniu jej w hook’u useEffect, początek naszego komponentu powinien wyglądać następująco:

const [todos,setTodos] = useState<ITodo[]>([]);
 const [loading,setLoading] = useState(true);

 useEffect(() => {
    getTasks();
 },[]);

 const getTasks = async () => {
    try {
        setLoading(true);
        const userId = supabase.auth.user()!.id;
        const { data: Todos, error, status } = await supabase
            .from('Todo')
            .select('*')
            .eq('user_id', userId);

        if (error && status !== 406) {
            throw error;
        }

        if (Todos) {
            setTodos(Todos)
        }
    }
    catch (error:any) {
        alert(error.message)
    }
    finally {
        setLoading(false)
    }
 }

Teraz możemy wykorzystać pobrane dane i wyświetlić je w postaci komponentów Todo korzystając z funkcji map:

{loading ?
    <div>Loading...</div> :
    <ul className="todo-list">
        {todos.map((todo) => <Todo/>)}
    </ul>
 }

Póki co nie widzimy żadnych zadań, gdyż żadnego jeszcze nie dodaliśmy. Możemy dodać zadanie bezpośrednio z panelu Supabase. Wejdź w zakładkę Authentication i skopiuj swoje id użytkownika.

Ponieważ zadanie powiązane jest z daną kategorią, musimy dodać również naszą pierwszą kategorię. W zakładce Table Editor wybierz tabelę Category i kliknij Insert row. W nowym panelu wprowadź dane dla nowej kategorii i wklej swoje id użytkownika:

Table Editor -> Category -> Insert row

W ten sam sposób dodaj pierwsze zadanie. Pamiętaj, żeby wskazać odpowiednią kategorię, a także podać swoje id użytkownika:

Table Editor -> To do -> Insert row
Table Editor -> To do -> Insert row

Jeżeli wszystko przebiegło poprawnie, powinniśmy zobaczyć dokładnie jedno zadanie. Dalej nie jest ono jednak wypełnione naszymi danymi, dlatego musimy przekazać pobrane dane do komponentu zadania. Przygotujmy zatem ten komponent:

import { FunctionComponent } from "react";
 import { ITodo } from "../models";

 interface TodoProps {
    todo: ITodo
 }

 const Todo: FunctionComponent<TodoProps> = ({todo}) => {

    return (
        <li className="todo">
            <p className="todo-date">{todo?.created_at}</p>
            <p className="todo-name">{todo?.name}</p>
            <p className="todo-date">Category: <span>{todo?.category_id}</span></p>
            <p className="todo-desc">{todo?.description}</p>
            <div className="btn-container">
                <button className="btn">Delete</button>
                <button className="btn">Edit</button>
            </div>
        </li>
    );
 }

 export default Todo;

Przekażmy do niego obiekt todo w metodzie map wraz z podaniem id zadania jako klucz:

<ul className="todo-list">
    {todos.map((todo) => <Todo key={todo.id} todo={todo}/>)}
 </ul>

W naszej aplikacji już widać dodane wcześniej zadanie!

Widok zadania w aplikacji
Widok zadania w aplikacji

W kolejnej części poradnika zajmiemy się poprawieniem komponentu Todo, a także kolejnymi funkcjonalnościami naszej aplikacji.

Link:  API z bazą danych w kilka minut? – poznaj Supabase (2/2)

Adam Lipiński

Za dnia początkujący front-end developer, w nocy zaś student (czasami na odwrót). Fan testowania nowych technologii oraz CSSowych sztuczek.

Zostaw komentarz

16 + 16 =

Top