DevGang
Авторизоваться

Веб-приложение Fullstack JavaScript: Nextjs и Docker

Существует множество технологий, но мы постараемся сделать пример как можно более простым, чтобы сделать его понятным.

Мы будем использовать подход "снизу вверх", начиная с базы данных и заканчивая интерфейсом.

Архитектура

Прежде чем мы начнем, вот простая схема, объясняющая архитектуру приложения.

Frontend - это Next.js приложение с TypeScript и Tailwind CSS. Backend - это Node.js приложение с Express и Prisma в качестве ORM. Database - PostgreSQL. Мы будем использовать Docker для запуска базы данных, серверной части, а также внешнего интерфейса (вы также можете использовать Vercel). Мы будем использовать Docker Compose для совместной работы интерфейса, серверной части и базы данных.

Необходимые требования

  • Базовые знания о том, что такое интерфейс, серверная часть, API и база данных
  • Docker установлен на вашем компьютере
  • Node.js установлено на вашем компьютере
  • Postman или любой другой инструмент для выполнения HTTP-запросов (необязательно)

1. Подготовка

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

mkdir <YOUR_FOLDER>
cd <YOUR_FOLDER>
code .

Инициализируйте репозиторий git.

git init
touch .gitignore

Заполните .gitignore файл следующим содержимым:

*node_modules

Создайте файл с именем compose.yaml в корневом каталоге проекта.

touch compose.yaml

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

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

После каждого шага мы будем тестировать текущее состояние приложения, чтобы убедиться, что все работает должным образом.

2. База данных

Мы будем использовать Postgres, но не устанавливать его на нашем компьютере. Вместо этого мы будем использовать Docker для запуска в контейнере. Таким образом, мы можем легко запускать и останавливать базу данных, не устанавливая ее на нашем компьютере.

Откройте файл compose.yaml и добавьте следующее содержимое:

version: '3.9'

services:
  db:
    container_name: db
    image: postgres:12
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - 5432:5432
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: {}

Затем введите в своем терминале:

docker compose up -d

Это приведет к извлечению образа Postgres из Docker Hub и запуску контейнера. Флаг -d означает, что контейнер будет работать в отключенном режиме, поэтому мы можем продолжать использовать терминал.

Проверьте, запущен ли контейнер:

docker ps -a

Шаг в контейнер db:

docker exec -it db psql -U postgres

Теперь, находясь в контейнере Postgres, вы можете ввести:

\l
\dt

И вы не должны увидеть никаких связей.

Теперь вы можете выйти из контейнера с помощью exit команды.

3. Серверная часть

Первый шаг сделан. Теперь мы создадим серверную часть. Мы будем использовать Node.js и Express. Мы также будем использовать Prisma для взаимодействия с базой данных.

Создайте папку с именем backend в корневом каталоге проекта.

mkdir backend

Затем откройте папку в терминале и инициализация проекта Node.js:

cd backend
npm init -y

Установите зависимости:

  • express: для создания сервера
  • prisma: для взаимодействия с базой данных
  • @prisma/client: для генерации кода для взаимодействия с базой данных
npm i express prisma @prisma/client

Инициализируем проект prisma:

npx prisma init

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

Откройте файл с именем .env и замените содержимое следующим:

DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public"

В качестве модели для нашего проекта мы будем использовать "Пользователя" с идентификатором, именем и электронной почтой. Идентификатор будет автоматически вводимым целым числом, имя будет строкой, и адрес электронной почты также будет строкой.

Откройте файл /prisma/schema.prisma и замените содержимое следующим:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

//User with id as int autoincrement, name as string, email as string
model User {
  id    Int    @id @default(autoincrement())
  name  String
  email String
}

Файл schema.prisma file должен выглядеть следующим образом:

Теперь создайте файл с именем index.js внутри /backend папки и добавьте следующее содержимое:

const express = require('express');
const { PrismaClient } = require('@prisma/client');

const prisma = new PrismaClient();
const app = express();

//use json
app.use(express.json());

//cors
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

//test api with error handling
app.get('/test', (req, res, next) => {
  try {
    res.status(200).json({ message: 'Success!' });
  } catch (err) {
    next(err);
  }
});

//get all users
app.get('/users', async (req, res, next) => {
  try {
    const users = await prisma.user.findMany();
    res.status(200).json(users);
  } catch (err) {
    next(err);
  }
});

//get user by id
app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await prisma.user.findUnique({
      where: { id: Number(req.params.id) },
    });
    res.status(200).json(user);
  } catch (err) {
    next(err);
  }
});

//create user
app.post('/users', async (req, res, next) => {
  try {
    const user = await prisma.user.create({
      data: { ...req.body },
    });
    res.status(201).json(user);
  } catch (err) {
    next(err);
  }
});

//update user
app.put('/users/:id', async (req, res, next) => {
  try {
    const user = await prisma.user.update({
      where: { id: Number(req.params.id) },
      data: { ...req.body },
    });
    res.status(200).json(user);
  } catch (err) {
    next(err);
  }
});

//delete user
app.delete('/users/:id', async (req, res, next) => {
  try {
    const user = await prisma.user.delete({
      where: { id: Number(req.params.id) },
    });
    res.status(200).json(user);
  } catch (err) {
    next(err);
  }
});

//Start server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Теперь вы можете сгенерировать схему Prisma:

npx prisma generate

Прежде чем мы настроим серверную часть, давайте протестируем ее. Введите в вашем терминале:

node index.js

Откройте ваш браузер и перейдите на http://localhost:4000/test. Вы должны увидеть сообщение Success!.

Но если мы продолжим localhost:4000/users, мы действительно увидим ошибку! Это потому, что у нас еще нет схемы в нашей базе данных:

Мы можем приступить к части докеризации, и мы решим эту проблему позже.

Доработайте серверную часть

Давайте создадим 2 файла с именами .dockerignore и backend.dockerfile в backend папке.

Откройте файл .dockerignore и добавьте следующее содержимое:

**/node_modules

Откройте файл backend.dockerfile и добавьте следующее содержимое:

FROM node:20

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY prisma ./prisma

RUN npx prisma generate

COPY . .

EXPOSE 4000

CMD ["node", "index.js"]

Давайте обновим compose.yaml файл в корневом каталоге проекта, добавив backend сервис.

Ниже представлена обновленная версия:

version: '3.9'

services:
  backend:
    container_name: backend
    image: backend
    build:
      context: ./backend
      dockerfile: backend.dockerfile
    ports:
      - "4000:4000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?schema=public
    depends_on:
      - db
  db:
    container_name: db
    image: postgres:12
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: {}

Создание образа серверной части:

docker compose build

Запустите серверный контейнер:

docker compose up -d backend

Проверьте, запущен ли контейнер:

docker ps -a

Кое-что интересное, прежде чем мы продолжим или сделаем какие-либо http-запросы.

docker exec -it db psql -U postgres
\dt

И мы не должны видеть никаких связей.

Теперь введите:

docker exec -it backend npx prisma migrate dev --name init
docker exec -it db psql -U postgres
\dt

И мы должны увидеть таблицу User в базе данных. Конечно, она все еще пустая, но Prisma создала ее для нас. Круто!

Добавлять пользователей можно тремя различными способами

Теперь давайте создадим 3 разных пользователя, используя 3 разных способа.

  • 1 пользователь, использующий Prisma Studio
  • 1 пользователь, использующий Postman
  • 1 пользователь, использующий psql

Создайте пользователя с помощью Prism Studio

Откройте новый терминал и введите:

npx prisma studio

Это откроет Prisma Studio в вашем браузере по адресу http://localhost:5555 (Примечание: мы не используем Docker для запуска Prisma Studio, мы запускаем его непосредственно на нашем компьютере).

Добавьте запись: userfromprisma и userfromprismamail

Нажмите Save 1 change. Вы можете оставить Prisma Studio открытой во вкладке, мы воспользуемся ею позже.

Теперь давайте добавим еще одного пользователя с помощью Postman (или любого другого инструмента, который вам нравится).

Если мы отправим http-запрос к http://localhost:4000/users, мы должны увидеть 1 пользователя (того, которого мы только что создали с помощью Prisma Studio).

{
  "name": "userfrompostman",
  "email": "userfrompostmanmail"
}

Если мы проверим localhost:4000/users, то сейчас должны увидеть 2 пользователя:

Мы также можем еще раз проверить согласованность наших операций в Prisma Studio.

Вставить пользователя из psql

Давайте вставим другого пользователя, используя psql.

docker exec -it db psql -U postgres
\dt

Давайте вставим нового пользователя вручную, используя psql (не делайте этого в процессе производства, это просто для тестирования!)

insert into "User" (name, email) values ('frompsql', 'userfrompsqlmail');
select * from "User";

Давайте еще раз проверим Prisma Studio, и теперь мы должны увидеть 3 пользователя:

4. Интерфейс

Теперь, когда у нас запущена серверная часть, мы можем приступить к интерфейсу. Мы будем использовать Next.js 14 с TypeScript и Tailwind.

Из корневой папки проекта,

cd ..

И из корневой папки проекта запустите эту команду:

npx create-next-app@latest --no-git

Мы используем флаг --no-git, потому что мы уже инициализировали репозиторий git в корневом каталоге проекта.

В качестве опций:

  • Как называется ваш проект? frontend
  • TypeScript? Yes
  • ESLint? Yes
  • Tailwind CSS? Yes
  • Используете структуру каталогов по умолчанию? Yes
  • Маршрутизатор приложений? No (не требуется для этого проекта)
  • Настроить псевдоним импорта по умолчанию? No

Это должно создать новый проект Next.js примерно за одну минуту.

Перейдите в папку frontend:

cd frontend

Установите Axios, мы будем использовать его для выполнения HTTP-запросов (обязательно в frontend папке):

npm i axios

Прежде чем мы продолжим, попробуйте запустить проект:

npm run dev

И откройте свой браузер по адресу http://localhost:3000. Вы должны увидеть страницу по умолчанию Next.js.

Измените файл styles/global.css

В src/frontend/src/styles/globals.css файле замените содержимое на это (чтобы избежать некоторых проблем с Tailwind):

@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --foreground-rgb: 0, 0, 0; 
  --background-start-rgb: 214, 219, 220;
  --background-end-rgb: 255, 255, 255;
}

body {
  color: rgb(var(--foreground-rgb));
  background: linear-gradient(
      to bottom,
      transparent,
      rgb(var(--background-end-rgb))
    )
    rgb(var(--background-start-rgb));
}

Создайте новый компонент

В /frontend/src папке создайте новую папку с именем components, а внутри нее создайте новый файл с именем CardComponent.tsx и добавьте следующее содержимое:

import React from 'react';

interface Card {
  id: number; 
  name: string;
  email: string;
}

const CardComponent: React.FC<{ card: Card }> = ({ card }) => {
  return (
    <div className="bg-white shadow-lg rounded-lg p-2 mb-2 hover:bg-gray-100">
      <div className="text-sm text-gray-600">ID: {card.id}</div>
      <div className="text-lg font-semibold text-gray-800">{card.name}</div>
      <div className="text-md text-gray-700">{card.email}</div>
    </div>
  );
};

export default CardComponent;

Файл должен выглядеть следующим образом:

Теперь откройте src/frontend/src/pages/index.tsx файл и замените содержимое следующим:

Заполните файл index.tsx

Откройте файл с именем index.tsx в frontend/src/pages папке и замените содержимое следующим:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import CardComponent from '../components/CardComponent';

interface User {
  id: number;
  name: string;
  email: string;
}

export default function Home() {
  const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000';
  const [users, setUsers] = useState<User[]>([]); 
  const [newUser, setNewUser] = useState({ name: '', email: '' }); 
  const [updateUser, setUpdateUser] = useState({ id: '', name: '', email: '' });

  // Fetch users
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get(`${apiUrl}/users`);
        setUsers(response.data.reverse());
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  // Create a user
  const createUser = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
      const response = await axios.post(`${apiUrl}/users`, newUser);
      setUsers([response.data, ...users]);
      setNewUser({ name: '', email: '' });
    } catch (error) {
      console.error('Error creating user:', error);
    }
  };

  // Update a user
  const handleUpdateUser = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
      await axios.put(`${apiUrl}/users/${updateUser.id}`, { name: updateUser.name, email: updateUser.email });
      setUpdateUser({ id: '', name: '', email: '' });
      setUsers(
        users.map((user) => {
          if (user.id === parseInt(updateUser.id)) {
            return { ...user, name: updateUser.name, email: updateUser.email };
          }
          return user;
        })
      );
    } catch (error) {
      console.error('Error updating user:', error);
    }
  };

  // Delete a user
  const deleteUser = async (userId: number) => {
    try {
      await axios.delete(`${apiUrl}/users/${userId}`);
      setUsers(users.filter((user) => user.id !== userId));
    } catch (error) {
      console.error('Error deleting user:', error);
    }
  };

  return (
    <main className="flex flex-col items-center justify-center min-h-screen p-4 bg-gray-100">
      <div className="space-y-4 w-full max-w-2xl">
        <h1 className="text-2xl font-bold text-gray-800 text-center">User Management App</h1>

        {/* Form to add new user */}
        <form onSubmit={createUser} className="p-4 bg-blue-100 rounded shadow">
          <input
            placeholder="Name"
            value={newUser.name}
            onChange={(e) => setNewUser({ ...newUser, name: e.target.value })}
            className="mb-2 w-full p-2 border border-gray-300 rounded"
          />

          <input
            placeholder="Email"
            value={newUser.email}
            onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
            className="mb-2 w-full p-2 border border-gray-300 rounded"
          />
          <button type="submit" className="w-full p-2 text-white bg-blue-500 rounded hover:bg-blue-600">
            Add User
          </button>
        </form>

        {/* Form to update user */}
        <form onSubmit={handleUpdateUser} className="p-4 bg-green-100 rounded shadow">
          <input
            placeholder="User ID"
            value={updateUser.id}
            onChange={(e) => setUpdateUser({ ...updateUser, id: e.target.value })}
            className="mb-2 w-full p-2 border border-gray-300 rounded"
          />
          <input
            placeholder="New Name"
            value={updateUser.name}
            onChange={(e) => setUpdateUser({ ...updateUser, name: e.target.value })}
            className="mb-2 w-full p-2 border border-gray-300 rounded"
          />
          <input
            placeholder="New Email"
            value={updateUser.email}
            onChange={(e) => setUpdateUser({ ...updateUser, email: e.target.value })}
            className="mb-2 w-full p-2 border border-gray-300 rounded"
          />
          <button type="submit" className="w-full p-2 text-white bg-green-500 rounded hover:bg-green-600">
            Update User
          </button>
        </form>

        {/* Display users */}
        <div className="space-y-2">
          {users.map((user) => (
            <div key={user.id} className="flex items-center justify-between bg-white p-4 rounded-lg shadow">
              <CardComponent card={user} />
              <button onClick={() => deleteUser(user.id)} className="bg-red-500 hover:bg-red-600 text-white py-2 px-4 rounded">
                Delete User
              </button>
            </div>
          ))}
        </div>
      </div>
    </main>
  );
}

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

Мы также можем обновить пользователя непосредственно из пользовательского интерфейса, добавив идентификатор пользователя, новое имя и новый адрес электронной почты.

Если мы проверим localhost:4000/users, мы сможем увидеть изменения, внесенные нами с помощью пользовательского интерфейса:

Вы также можете удалить пользователя прямо из пользовательского интерфейса:

Теперь остановите Next.js приложение, нажав Ctrl-C или Cmd-c, чтобы мы оставили порт 3000 доступным для докеризованного Next.js приложения.

Доработайте интерфейс

Развертывание Next.js приложения с помощью Docker.

Измените next.config.js файл в frontend папке, заменив его следующим содержимым:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone'
}

module.exports = nextConfig

Создайте файл с именем .dockerignore в frontend папке и добавьте следующее содержимое:

**/node_modules

Создайте файл с именем frontend.dockerfile в frontend папке и добавьте следующее содержимое (оно взято непосредственно из официального примера docker от vercel).

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfile not found." && exit 1; \
  fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN yarn build && ls -l /app/.next


# If using npm comment out above and use below instead
# RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000
# set hostname to localhost
ENV HOSTNAME "0.0.0.0"

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD ["node", "server.js"]

Теперь давайте обновим docker-compose.yml файл в корневом каталоге проекта, добавив frontend сервис.

Ниже представлена обновленная версия:

version: '3.9'

services:
  frontend:
    container_name: frontend
    image: frontend
    build:
      context: ./frontend
      dockerfile: frontend.dockerfile
    ports:
      - '3000:3000'
    environment:
      - NEXT_PUBLIC_API_URL=http://localhost:4000
    restart: always
    depends_on:
      - backend
  backend:
    container_name: backend
    image: backend
    build:
      context: ./backend
      dockerfile: backend.dockerfile
    ports:
      - '4000:4000'
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres?schema=public
    depends_on:
      - db
  db:
    container_name: db
    image: postgres:12
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgresD
      POSTGRES_DB: postgres
    ports:
      - 5432:5432
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata: {}

Создание образа интерфейса:

docker compose build

Запустите интерфейсный контейнер:

docker compose up -d frontend

5. Финальный тест

Сначала давайте проверим, запущены ли все 3 контейнера:

docker ps -a

Теперь откройте свой браузер по адресу http://localhost:3000. Вы должны увидеть запущенное приложение, но на этот раз мы запускаем его с помощью Docker.

Вы можете, например, создать нового пользователя прямо из пользовательского интерфейса:

Заключение

Мы создали приложение с полным стеком, используя Docker для запуска базы данных, серверной части и интерфейса. Мы также использовали Docker Compose для совместной работы базы данных, серверной части и интерфейса.

Мы использовали множество технологий, но на нашем компьютере ничего не устанавливали, кроме Docker и Node.js.

  • Next.js 14 (TypeScript)
  • Tailwind CSS
  • Node.js
  • Express (JavaScript)
  • Prisma
  • PostgreSQL
  • Docker
  • Docker Compose

Весь код доступен бесплатно на GitHub.

Источник:

#JavaScript #PostgreSQL #NodeJS #Docker #TypeScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу