# Serverless To-Do App mit Vercel, Node, Vue & Airtable

Heute zeige ich dir, wie du ein MVP (Minimal Viable Product) ohne Infrastruktur-Kosten erstellen kannst. Dazu erstelle ich mit dir eine To-Do App.

Voraussetzung

  • Airtable Account
  • Node.js installiert

Als erstes meldest du dich bei Airtable an und legst eine neu base an ("start from scratch").

Dann öffne die base und passe den Tabellennamen und die Spalten wie folgt an: Nun erstellen wir die API mit folgenden Befehlen:

mkdir todo-api
cd todo-api
npm init -y
npm i express axios cors body-parser

Danach erstellst du eine index.js mit folgendem Inhalt:

    //index.js
    const express = require('express');
    const cors = require("cors");
    const bodyParser = require('body-parser');
    
    const todo = require('./todo');
    
    var server = express();
    server.use(bodyParser.json());
    server.use(cors());
    
    //Note routes
    server.get('/todo', todo.list);
    server.post('/todo', todo.create);
    server.put('/todo/:id', todo.update);
    server.delete('/todo/:id', todo.delete);
    
    server.listen(3000, function () {
        console.log('server listening on port 3000!');
    });

Für die Interaktion mit Airtable erstellst Du die todo.js. Dafür benötigst Du deinen:

//todo.js
const axios = require('axios');
const apiToken = "keyXXXXXXXXXX"
const airTableApp = "appXXXXXXXXXX"
const airTableName = "todo"

exports.list = async function(req, res) {
    let todos = [];
    try{
        const response = await axios.get(`https://api.airtable.com/v0/${airTableApp}/${airTableName}`,
            { headers: { Authorization: "Bearer " + apiToken }});

            todos = response.data.records;
    }
    catch(err){
        console.log(err);
    }
    res.send(todos);
};


exports.create = async function(req, res) {
    
    try{
        let todo = req.body;

        if(todo.fields === undefined){
            throw "Invalid object";
        }
        if(todo.fields.title === undefined){
            throw "Invalid object";
        }

        //Create todo 
        response = await axios.post(`https://api.airtable.com/v0/${airTableApp}/${airTableName}`,
            todo,
            { 
                headers: { 
                    Authorization: "Bearer " + apiToken,
                    'Content-Type': 'application/json'
                }
            });

        res.send("OK");
    }
    catch(err){
        console.log(err);
        res.send(err);
    }
    
};

exports.update = async function(req, res) {
    
    try{
        let todo = req.body;
        delete todo.createdTime;
    
        let todos = {};
        todos.records = [];
        todos.records.push(todo);
        
        const response = await axios.patch(`https://api.airtable.com/v0/${airTableApp}/${airTableName}`,
            todos,
            { 
                headers: { 
                    Authorization: "Bearer " + apiToken,
                    'Content-Type': 'application/json'
                }
            });
            res.send("OK");
    }
    catch(err){
        console.log(err.response.status);
        console.log(err.response.data.error.message);

        res.send("NOK");
    }
    
    
};

exports.delete = async function(req, res) {
    
    try{
        const response = await axios.delete(`https://api.airtable.com/v0/${airTableApp}/${airTableName}`,
            {
                params: {
                    'records[]': req.params.id
                },
                headers: { 
                    Authorization: "Bearer " + apiToken,
                    'Content-Type': 'application/x-www-form-urlencoded'
                }
            }
        );

        res.send("OK");
    }
    catch(err){
        console.log(err);
        res.send("NOK");
    }
    
    
};

Danach kannst du deine API mit "node index.js" starten. Du erreichst sie nun über  http://localhost:3000 (opens new window)

Als nächstes erstellst die Vue.js App. Dazu führst du folgende Befehle auf der Konsole aus:

mkdir todo-app
cd todo-app
npm init -y
npm i vue vuetify axios
npm i -g parcel-bundler

Danach erstellst du die Dateien index.html, main.js und App.vue und füllst diese mit folgendem Code:

index.html

  <!--index.html-->
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, height=device-height, viewport-fit=cover"/>
    <title>App</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="main.js"></script>
  </body>
  <style>
  </style>
  </html>

main.js

    //main.js
    import Vue from "vue";
    import Vuetify from "vuetify";
    import "vuetify/dist/vuetify.min.css";
    import App from "./App.vue";
    
    Vue.config.productionTip = false;
    Vue.use(Vuetify);
    const vuetify = new Vuetify({
      theme: {
        dark: false,
      },
    });
    
    new Vue({
      vuetify,
      render: (h) => h(App)
    }).$mount("#app");

App.vue

<!--App.vue-->
<template>
  <v-app>
    <v-main>
        <v-container v-if="loggedIn">
                <v-row justify="center" class="ma-5">
                  <v-col xs="12" sm="8">
                    <v-card>
                      <v-toolbar color="blue darken-4" dark>
                        
                        <v-toolbar-title class="headline">Todo App</v-toolbar-title>
      
                        <v-spacer></v-spacer>
    
                            <v-btn icon @click="isDark = !isDark">
                              <v-icon v-model="isDark">{{ !isDark ? 'mdi-weather-night' : 'mdi-weather-cloudy' }}</v-icon>
                            </v-btn>
  
                      </v-toolbar>
      
                      <v-list two-line subheader>
                        <v-subheader class="headline"></v-subheader>
                        <p class="mx-12 text-right"><b>{{todos.length}}</b> Tasks</p>
      
                        <v-list-item>
                          <v-list-item-content>
                            <v-list-item-title>
      
                              <v-text-field v-model="newTodo" id="newTodo" name="newTodo" label="Type your task" @keyup.enter="addTodo" />
                            </v-list-item-title>
                          </v-list-item-content>
                        </v-list-item>
      
                      </v-list>
      
                      <v-list subheader two-line flat>
                        <v-subheader class="subheading" v-if="todos.length == 0">You have 0 Tasks, add some</v-subheader>
                        <v-subheader class="subheading" v-else="todos.length == 1">Your Tasks</v-subheader>
      
                        <v-list-item-group>
                          <v-list-item v-for="(todo, i) in todos">
                            
                              <v-list-item-action>
                                <v-checkbox value="true" v-model="todo.fields.done" @click="toggle(todo)"></v-checkbox>
                              </v-list-item-action>
      
                              <v-list-item-content :class="{done : (todo.fields.done === 'true')}">
                                <v-list-item-title>{{ todo.fields.title }}</v-list-item-title>
                                <v-list-item-subtitle>Added on: {{ todo.createdTime }}</v-list-item-subtitle>
                              </v-list-item-content>
                              <v-btn fab ripple small color="red" v-if="todo.fields.done === 'true'" @click="removeTodo(todo)">
                                <v-icon class="white--text">mdi-close</v-icon>
                              </v-btn>
                            
                          </v-list-item>
                        </v-list-item-group>
                      </v-list>
                    </v-card>
                  </v-col>
                </v-row>
              </v-container>
    </v-main>
  </v-app>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      newTodo: '',
      todos: []
    }
  },
    mounted(){
        this.getTodos();
    },
  methods: {
    getTodos(){
        axios
        .get("https://micro-saas-api.vercel.app/notes")
        .then((response) => {
            this.todos = response.data;
        })
        .catch((error) => {
            console.log(error);
        });

    },
    toggle(note){

        axios
        .put("https://micro-saas-api.vercel.app/note/" + note.id, 
        note)
        .then((response) => {
            this.getTodos();
        })
        .catch((error) => {
            console.log(error);
        });

    },
    addTodo() {
      var value = this.newTodo && this.newTodo.trim();
      if (!value) {
        return;
      }
      this.newTodo = "";

      let note = {};
      note.fields = {};
      note.fields.title = value;
      note.fields.userid = this.userid;
      note.fields.done = "false";

      axios
        .post("https://micro-saas-api.vercel.app/note", 
        note)
        .then((response) => {
            this.getTodos();
        })
        .catch((error) => {
            console.log(error);
        });
      
      
    },
    removeTodo(note) {
        axios
        .delete("https://micro-saas-api.vercel.app/note/" + note.id,
        {
            headers: {
                Authorization: "Basic " + this.auth,
            },
        })
        .then((response) => {
            this.getTodos();
        })
        .catch((error) => {
            console.log(error);
        });
    }
  }
}
</script>

<style>
</style>

Mit dem Befehl "parcel index.html" kannst Du das Ergebnis lokal testen (meist http://localhost:1234).

Beim nächsten mal zeige ich dir, wie du eine Authentifizierung in die App bekommst.

Last Updated: 9/6/2021, 9:06:41 PM