Создание приложения для облачных вычислений с помощью Nitric

Обзор

Nitric — это бессерверный фреймворк для быстрой разработки и развертывания облачных приложений. В этом руководстве показано, как создать простой список дел с помощью Next.js с API, поддерживаемым функциями Nitric. В нем также используются коллекции для сохранения данных. Изначально оно было опубликовано здесь, на сайте Nitric.

В руководстве мы будем использовать следующее:

  • API, функции и коллекции Nitric
  • Next.js
  • TailwindCSS для стилизации нашего фронтенда
  • Облако по вашему выбору:
    • AWS
    • GCP
    • Azure

Предварительные условия

  • Node.js
  • Next.js
  • Nitric CLI

Начните с приложения Next.js

Мы начнем с готового продукта и будем следовать этому пути, просто клонируйте проект Nitric to-do с GitHub.

git clone https://github.com/nitrictech/nitric-todo.git
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Установите зависимости с помощью npm или yarn.

cd nitric-todo
yarn install
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Далее откройте проект в выбранном вами редакторе.

code .
Войдите в полноэкранный режим Выйти из полноэкранного режима

Структура проекта

Проект разделен на две основные области:

  • todo-api — Здесь хранится API Nitric.
  • web — Здесь хранится ваше приложение Next.js.

Типизация

Мы добавим несколько типов для наших задач и наших api запросов/ответов.

// todo-api/types.ts

/* Base Types */
export interface Task {
  id: string;
  createdAt: number;
  name: string;
  complete: boolean;
  description?: string;
  dueDate?: number;
}

export interface TaskList {
  id: string;
  createdAt: number;
  name: string;
  tasks: Task[];
}

/* Task List */
export type Filters = Partial<Task>;

export type TaskListResponse = TaskList;

export type TaskListRequest = Omit<TaskList, 'id' | 'tasks'>;

export type TaskListPostRequest = Omit<TaskList, 'id' | 'complete'>;

/* Task Post */
export type TaskPostRequest = Omit<Task, 'id'>;

/* Task Update */
export type TaskPatchRequest = { completed: boolean };
Вход в полноэкранный режим Выход из полноэкранного режима

Ресурсы

Приложения, созданные с помощью Nitric, определяют свои ресурсы в коде, вы можете написать это в корне любого файла .js или .ts, но для организации мы рекомендуем собрать их вместе. Итак, давайте начнем с определения ресурсов, которые нам понадобятся для поддержки нашего API, в новом каталоге resources.

// todo-api/resources/apis.ts

import { api } from '@nitric/sdk';

export const taskListApi = api('taskList');
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы также хотим создать коллекцию для хранения списков задач. Мы опустим задачи, чтобы вместо этого хранить их в виде подколлекций. Это облегчит поиск отдельных задач в будущем.

// todo-api/resources/collections.ts

import { collections } from '@nitric/sdk';
import { TaskList } from 'types';

type TaskCollection = Omit<TaskList, 'tasks'>;

export const taskListCol = collection<TaskCollection>('taskLists');
Вход в полноэкранный режим Выход из полноэкранного режима

Маршруты

Начните настраивать маршруты API, они могут оставаться пустыми функциями, пока мы их не заполним.

// todo-api/functions/tasks.ts

import { taskListApi } from '../resources/apis.ts';

taskListApi.get("/:listid/:id", async (ctx) => {});      // Get task with [id]
taskListApi.get("/:listid", async (ctx) => {);           // Get task list with [id]
taskListApi.get("/", async (ctx) => {});                 // Get all task lists
taskListApi.post("/:listid", async (ctx) => {});         // Post new task for task list
taskListApi.post("/", async (ctx) => {});                // Post new task list
taskListApi.patch("/:listid/:id", async (ctx) => {});    // Update task
taskListApi.delete("/:listid", async (ctx) => {});       // Delete task list
taskListApi.delete("/:listid/:id", async (ctx) => {});   // Delete task
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем мы можем получить нашу предыдущую коллекцию и применить к ней разрешения для использования в этой функции.

// todo-api/functions/tasks.ts

import { taskListCol } from '../resources/collections.ts';

const taskLists = taskListCol.for('reading', 'writing', 'deleting');
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь, когда у нас есть коллекция, мы можем начать добавлять задачи и списки задач. Мы используем коллекцию для хранения списков задач, а затем подколлекцию в каждом списке задач для хранения задач.

Сначала мы можем разместить новый список задач, используя конечную точку POST /.

// todo-api/functions/tasks.ts

taskListApi.post('/', async (ctx) => {
  const { name, tasks } = ctx.req.json() as TaskListPostRequest;

  try {
    if (!name) {
      ctx.res.body = 'A new task list requires a name';
      ctx.res.status = 400;
      return;
    }

    const id = uuid.generate();

    await taskLists.doc(id).set({
      id,
      name,
      createdAt: new Date().getTime(),
    });

    // add any tasks if supplied
    if (tasks) {
      for (const task of tasks) {
        const taskId = uuid.generate();
        await taskLists
          .doc(id)
          .collection<Task>('tasks')
          .doc(taskId)
          .set({
            ...task,
            complete: false,
            createdAt: new Date().getTime(),
          });
      }
    }

    ctx.res.body = 'Successfully added task list!';
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to add task list';
    ctx.res.status = 400;
  }

  return ctx;
});
Вход в полноэкранный режим Выйти из полноэкранного режима

Используя POST /:listid, мы можем поместить новую задачу в список задач, добавив новый документ в подколлекцию списка задач.

Сначала мы получаем идентификатор списка задач, а затем добавляем новую задачу в подколлекцию listid -> tasks.

// todo-api/functions/tasks.ts

taskListApi.post('/:listid', async (ctx) => {
  const { listid } = ctx.req.params;
  const task = ctx.req.json() as TaskPostRequest;

  try {
    if (!listid) {
      ctx.res.body = 'A task list id is required';
      ctx.res.status = 400;
      return;
    }

    if (!task || !task.name) {
      ctx.res.body = 'A task with a name is required';
      ctx.res.status = 400;
      return;
    }

    const taskId = uuid.generate();

    await taskLists
      .doc(listid)
      .collection<Omit<Task, 'id'>>('tasks')
      .doc(taskId)
      .set({
        ...task,
        complete: false,
        createdAt: new Date().getTime(),
      });

    ctx.res.body = 'Successfully added task!';
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to add task list';
    ctx.res.status = 400;
  }

  return ctx;
});
Вход в полноэкранный режим Выход из полноэкранного режима

GET / вернет все списки задач и их задачи, отсортировав их по дате создания.

// todo-api/functions/tasks.ts

import { sortByCreatedAt } from "../common/utils";
...
taskListApi.get("/", async (ctx) => {
  try {
    const taskList = await taskLists.query().fetch();

    const taskListsWithTasks = await Promise.all(
      taskList.documents.map(async (doc) => {
        const { documents: tasks } = await taskLists
          .doc(doc.id)
          .collection<Task>("tasks")
          .query()
          .fetch();

        return {
          id: doc.id,
          ...doc.content,
          tasks: tasks
            .map(({ id, content }) => ({ id, ...content }))
            .sort(sortByCreatedAt),
        };
      })
    );

    ctx.res.json(taskListsWithTasks.sort(sortByCreatedAt));
  } catch (err) {
    console.log(err);
    ctx.res.body = "Failed to retrieve taskList list";
    ctx.res.status = 400;
  }

  return ctx;
});
Войти в полноэкранный режим Выход из полноэкранного режима
// todo-api/common/utils.ts

import { Task } from 'types';

type CreatedAtData = Pick<Task, 'createdAt'>;

export const sortByCreatedAt = (a: CreatedAtData, b: CreatedAtData) => {
  return a.createdAt < b.createdAt ? 1 : -1;
};
Войти в полноэкранный режим Выйти из полноэкранного режима

В конечной точке GET /:listid мы можем получить один список и применить фильтры к нашему запросу задач.

// todo-api/functions/tasks.ts

// Get all tasks from a task list, with filters
taskListApi.get('/:listid', async (ctx) => {
  const { listid } = ctx.req.params;
  const filters = ctx.req.query as Filters;

  try {
    const taskListRef = taskLists.doc(listid);
    let query = taskListRef.collection<Task>('tasks').query();

    // Apply filters to query before executing query;
    Object.entries(filters).forEach(([k, v]) => {
      switch (k) {
        case 'complete': {
          query = query.where(k, '==', v === 'true');
          break;
        }
        case 'dueDate': {
          query = query.where(k, '>=', v);
          break;
        }
        default: {
          query = query.where(k, 'startsWith', v as string);
          break;
        }
      }
    });

    const taskList = await taskListRef.get();
    const tasks = await query.fetch();

    ctx.res.json({
      ...taskList,
      tasks: tasks.documents
        .map((doc) => ({ id: doc.id, ...doc.content }))
        .sort(sortByCreatedAt),
    });
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to retrieve tasks';
    ctx.res.status = 400;
  }

  return ctx;
});
Войти в полноэкранный режим Выход из полноэкранного режима

В конечной точке GET /:listid/:id мы можем начать получать конкретные задачи из конкретных списков.

// todo-api/functions/tasks.ts

taskListApi.get('/:listid/:id', async (ctx) => {
  const { listid, id } = ctx.req.params;

  try {
    // Get our task list with id [listId]
    const taskListRef = taskListCol.doc(listid);
    // Get all tasks from the collection with id [id]
    const task = await taskListRef.collection<Task>('tasks').doc(id).get();

    ctx.res.json(task);
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to retrieve tasks';
    ctx.res.status = 400;
  }

  return ctx;
});
Вход в полноэкранный режим Выход из полноэкранного режима

В маршруте патча PATCH :listid/:id мы можем написать логику для обновления того, была ли задача выполнена.

// todo-api/functions/tasks.ts

taskListApi.patch('/:listid/:id', async (ctx) => {
  const { listid: listId, id } = ctx.req.params;
  const { completed } = ctx.req.json() as ToggleRequest;

  try {
    const taskListRef = taskLists.doc(listId);
    const taskRef = taskListRef.collection<Task>('tasks').doc(id);
    const originalTask = await taskRef.get();

    await taskListRef
      .collection<Task>('tasks')
      .doc(id)
      .set({
        ...originalTask,
        complete: completed,
      });

    ctx.res.body = 'Successfully updated task';
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to retrieve tasks';
    ctx.res.status = 400;
  }

  return ctx;
});
Вход в полноэкранный режим Выйти из полноэкранного режима

Маршруты удаления DELETE /:listid/:id и DELETE /:id получают соответствующие документы и удаляют их из коллекции.

// todo-api/functions/tasks.ts

taskListApi.delete('/:listid/:id', async (ctx) => {
  const { listid: listId, id } = ctx.req.params;

  try {
    const taskListRef = taskLists.doc(listId);
    await taskListRef.collection('tasks').doc(id).delete();
    ctx.res.body = 'Successfully deleted task';
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to delete task';
    ctx.res.status = 400;
  }

  return ctx;
});

taskListApi.delete('/:id', async (ctx) => {
  const { id } = ctx.req.params;

  try {
    await taskLists.doc(id).delete();
    ctx.res.body = 'Successfully deleted task list';
  } catch (err) {
    console.log(err);
    ctx.res.body = 'Failed to delete task list';
    ctx.res.status = 400;
  }

  return ctx;
});
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка API-прокси

Начните с создания своего файла .env, переименовав файл .env.example:

mv web/.env.example web/.env
Вход в полноэкранный режим Выйти из полноэкранного режима

В файле next.config.js у вас должны быть определены рерайты для прокси между вашим универсальным маршрутом Next.js API и API Nitric. Он принимает переменную API_BASE_URL, которая определена в файле .env.

// web/next.config.js

module.exports = {
  reactStrictMode: true,
  api: {
    bodyParser: {
      bodyParser: false, // Disallow body parsing, consume as stream
    },
  },
  // To avoid any CORs issues use Next.js as a proxy for Nitric API
  // We are working on it :)
  async rewrites() {
    return [
      {
        source: '/apis/:path*',
        destination: `${process.env.API_BASE_URL}/apis/:path*`, // Proxy to Backend
      },
    ];
  },
};
Вход в полноэкранный режим Выход из полноэкранного режима

Запуск локально

Чтобы протестировать наш api локально, мы можем сделать следующее:

cd todo-api
nitric run
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы можем запустить фронтенд Next.js с помощью:

cd ../web
yarn dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Развертывание

Развертывание API Nitric

Настройте учетные данные и любую другую конфигурацию, специфичную для облака:

  • AWS
  • Azure
  • GCP

Запустите команду развертывания.

Предупреждение: Публикация служб в облаке может повлечь за собой расходы.

nitric stack up -s todo
Вход в полноэкранный режим Выход из полноэкранного режима

Когда развертывание будет завершено, перейдите в соответствующую консоль облака, и вы сможете видеть и взаимодействовать с вашим API.

Когда вы закончите, вы можете уничтожить стек с помощью nitric down -s todo.

Развертывание приложения Next.js

Выберите одну из следующих кнопок развертывания и не забудьте обновить переменную API_BASE_URL во время этого процесса установки с развернутым api url.

Развернуть на Vercel

Развернуть на Netlify

*Примечание: Файл Netlify.toml в этом репозитории включает конфигурацию для настройки свойства API_BASE_URL при первоначальном развертывании.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *