Вот простой пример разработки базового приложения для блога с использованием NextJs 9 и Serverless Framework и его развертывания на AWS lambda, API Gateway и Zeit. Я использовал Amazon Dynamo DB в качестве базы данных для выполнения операций CRUD.

Вы можете найти образец репозитория здесь. И попробуйте это проверить.

И если вы хотите практиковаться и иметь представление о разработке приложения в целом. Ниже приведены шаги, которые вам необходимо выполнить:

Используемый стек:

Разработка REST API в бессерверной среде и развертывание в AWS lambda и API Gateway

  • Установите бессерверный
npm install -g serverless
  • Создать каталог проекта
serverless create -t aws-nodejs -p <app-name>
  • создать package.json
npm init
  • Настройте учетные данные AWS для развертывания
serverless config credentials --provider aws --key {your access key id} --secret {your secret access key}
  • Настройте Dynamo DB в serverless.yml
  • Добавьте следующий фрагмент, чтобы создать таблицу BLOG_TABLE в Dynamodb при первом развертывании:
provider:
  name: aws
  runtime: nodejs10.x
  region: ap-south-1
  environment:
    BLOG_TABLE: ${self:service}-${opt:stage, self:provider.stage}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:Query
        - dynamodb:PutItem
        - dynamodb:GetItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.BLOG_TABLE}"
resources:
  Resources:
    BlogsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.BLOG_TABLE}
        AttributeDefinitions:
          - AttributeName: user_id
            AttributeType: S
          - AttributeName: timestamp
            AttributeType: N
          - AttributeName: blog_id
            AttributeType: S
        KeySchema:
          - AttributeName: user_id
            KeyType: HASH
          - AttributeName: blog_id
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        GlobalSecondaryIndexes:
          - IndexName: timestamp-index
            KeySchema:
              - AttributeName: timestamp
                KeyType: HASH
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 1
              WriteCapacityUnits: 1
  • Добавьте файл конфигурации БД для приложения «config / db_config.js»:
const AWS = require('aws-sdk');
AWS.config.update({ region: 'ap-south-1' });
const dynamodb = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.BLOG_TABLE;
module.exports = {
	dynamodb, tableName
};
  • Добавьте некоторые общие служебные функции для проверки с использованием заголовков в файл: ‘util / headers.js
const getUserId = (headers) => {
    return headers.app_user_id;
}
const getUserName = (headers) => {
    return headers.app_user_name;
}
const getResponseHeaders = () => {
    return {
	"Access-Control-Allow-Origin": "*"
    }
}

module.exports = {
    getUserId,
    getUserName,
    getResponseHeaders
};
  • Настройте разрешенные заголовки для запросов в serverless.yml
custom:
  allowedHeaders:
    - Accept
    - Content-Type
    - Authorization
    - app_user_id
    - app_user_name
  • Установите несколько пакетов утилит npm: momentJs, uuid
npm install --save moment uuid
  • Создайте обработчики лямбда (лямбда-функции CRUD) в handler.js
'use strict';
const db = require('./config/db_config.js');
const util = require('./utils/headers.js');
const moment = require('moment');
const uuid = require('uuid/v4');
module.exports.getBlogs = async event => {
  try {
    let user_id = util.getUserId(event.headers);
    let params = {
      TableName: db.tableName,
      KeyConditionExpression: "user_id = :uid",
      ExpressionAttributeValues: {
        ":uid": user_id
      },
      ScanIndexForward: false
    }
    let data = await db.dynamodb.query(params).promise();
    return {
      statusCode: 200,
      headers: util.getResponseHeaders(),
      body: JSON.stringify(data)
    }
  } catch (err) {
    console.log("Error", err);
    return {
      statusCode: err.statusCode ? err.statusCode : 500,
      body: JSON.stringify({
        error: err.name ? err.name : "Exception",
        message: err.message ? err.message : "Unknown Error"
      })
    };
  }
};
module.exports.getBlog = async event => {
  try {
  
    let blog_id = event.pathParameters.blog_id;
    let params = {
      TableName: db.tableName,
      Key: {
        user_id: util.getUserId(event.headers),
        blog_id: blog_id
      }
    }
    let data = await db.dynamodb.get(params).promise();
    return {
      statusCode: 200,
      headers: util.getResponseHeaders(),
      body: JSON.stringify(data)
    }
  } catch (err) {
    console.log("Error", err);
    return {
      statusCode: err.statusCode ? err.statusCode : 500,
      body: JSON.stringify({
        error: err.name ? err.name : "Exception",
        message: err.message ? err.message : "Unknown Error"
      })
    };
  }
};
module.exports.addBlog = async event => {
  try {
    let item = JSON.parse(event.body).Item;
    item.user_id = util.getUserId(event.headers);    
    item.user_name = util.getUserName(event.headers);    
    item.blog_id = item.user_id + ':' + uuid()
    item.timestamp = moment().unix()
    let data = await db.dynamodb.put({
      TableName: db.tableName,
      Item: item
    }).promise();
    return {
      statusCode: 200,
      headers: util.getResponseHeaders(),
      body: JSON.stringify(item)
    }
  } catch (err) {
    console.log("Error", err);
    return {
      statusCode: err.statusCode ? err.statusCode : 500,
      body: JSON.stringify({
        error: err.name ? err.name : "Exception",
        message: err.message ? err.message : "Unknown Error"
      })
    };
  }
};
module.exports.updateBlog = async event => {
  try {
    let item = JSON.parse(event.body).Item;
    item.user_id = util.getUserId(event.headers);    
    item.user_name = util.getUserName(event.headers);    
    let data = await db.dynamodb.put({
      TableName: db.tableName,
      Item: item,
      ConditionExpression: '#t = :t',
      ExpressionAttributeNames: {
        '#t': 'timestamp'
      },
      ExpressionAttributeValues: {
        ':t': item.timestamp
      }
    }).promise();
    return {
      statusCode: 200,
      headers: util.getResponseHeaders(),
      body: JSON.stringify(item)
    }
  } catch (err) {
    console.log("Error", err);
    return {
      statusCode: err.statusCode ? err.statusCode : 500,
      body: JSON.stringify({
        error: err.name ? err.name : "Exception",
        message: err.message ? err.message : "Unknown Error"
      })
    };
  }
};
module.exports.deleteBlog = async event => {
  try {
    let blog_id = event.pathParameters.blog_id;
    let params = {
      TableName: db.tableName,
      Key: {
        user_id: util.getUserId(event.headers),
        blog_id: blog_id
      }
    }
    let data = await db.dynamodb.delete(params).promise();
    return {
      statusCode: 200,
      headers: util.getResponseHeaders(),
      body: JSON.stringify(data)
    }
  } catch (err) {
    console.log("Error", err);
    return {
      statusCode: err.statusCode ? err.statusCode : 500,
      body: JSON.stringify({
        error: err.name ? err.name : "Exception",
        message: err.message ? err.message : "Unknown Error"
      })
    };
  }
};
  • Отображение обработчиков лямбда
  • Добавьте следующий фрагмент кода, чтобы сопоставить каждый обработчик лямбда-выражения с типом метода, чтобы во время развертывания он мог создавать маршруты для каждого обработчика лямбда-выражений в шлюзе API.
functions:
  addBlog:
    handler: handler.addBlog
    events:
      - http:
          path: blogs
          method: post
          cors:
            origin: '*'
            headers: ${self:custom.allowedHeaders}
  getBlog:
    handler: handler.getBlog
    events:
      - http:
          path: blogs/{blog_id}
          method: get
          cors:
            origin: '*'
            headers: ${self:custom.allowedHeaders}
  getBlogs:
    handler: handler.getBlogs
    events:
      - http:
          path: blogs/
          method: get
          cors:
            origin: '*'
            headers: ${self:custom.allowedHeaders}
  updateBlog:
    handler: handler.updateBlog
    events:
      - http:
          path: blogs/
          method: put
          cors:
            origin: '*'
            headers: ${self:custom.allowedHeaders}
  deleteBlog:
    handler: handler.deleteBlog
    events:
      - http:
          path: blogs/{blog_id}
          method: delete
          cors:
            origin: '*'
            headers: ${self:custom.allowedHeaders}
  • Чтобы протестировать API локально, вам необходимо установить пакет serverless-offline.
npm run serverless-offline

а затем запустить

serverless offline

он разместит ваш apis на http: // localhost: 3000 / ‹path›

  • Развертывайте API в AWS с помощью
serverless deploy

Разработка FRONT-END с использованием NextJS 9 и его развертывание в Zeit.

  • Установите NextJS, React и React DOM
npm install --save next react react-dom
  • Установить axios
npm install --save axios
  • Создайте каталог в корневой папке проекта как «компоненты» и добавьте компонент заголовка навигации и базовый компонент макета приложения:

header.js

import Link from 'next/link';
const linkStyle = {
  marginRight: 15
};
const Header = () => (
  <div>
    <Link href="/">
      <a style={linkStyle}>Home</a>
    </Link>
    <Link href="blogs/create">
      <a style={linkStyle}>Create</a>
    </Link>
  </div>
);
export default Header;

basicLayout.js

import Header from './header.js';
const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
};
const Layout = props => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
);
export default Layout;
  • Создайте каталог в корневой папке проекта как «страницы» и добавьте index.js, который в основном является домашней страницей, со следующим фрагментом, который будет извлекать все блоги и отображаться в виде списка на домашней странице.

Убедитесь, что вы заменили ‹маршрут API› развернутым маршрутом API для получения всех блогов.

import Layout from '../components/basicLayout';
import Link from 'next/link';
import axios from 'axios';
const Index = props => (
  <Layout>
    <h1>Blogs</h1>
    <ul>
      {props.blogs.map(blog => (
        <li key={blog.blog_id}>
          <Link href="/blogs/[blog_id]" as={`/blogs/${blog.blog_id}`}>
            <a>{blog.title}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
);
Index.getInitialProps = async function() {
  const res = await axios.get(<APIroute>, {headers: {"app_user_id":"test", "app_user_name": "test"}});
  return {
    blogs: res.data.Items
  };
};
export default Index;
  • Создать форму блога
import Layout from '../../components/basicLayout';
import { Component } from 'react';
import Router from 'next/router'
import axios from 'axios';
class BlogForm extends Component {
  constructor(props) {
  	super(props);
  	this.state = {
  		title: '',
  		content: ''
  	};
  	this.submitForm = this.submitForm.bind(this);
  	this.handleChange = this.handleChange.bind(this);
  }
  handleChange(key) {
    return function (e) {
      var state = {};
      state[key] = e.target.value;
      this.setState(state);
    }.bind(this);
  }
  submitForm(event) {
    	event.preventDefault();
	  	
	  	let payload = {
	  		"Item": this.state
	  	};
	  	axios.post(
	  		'<API route>',
	  		 payload,
	  		 {headers: {"app_user_id":"test", "app_user_name": "test"}}
  		).then(response => {
  			Router.push('/');
  		});
  }
  render() {
    return (
      <Layout>
    	<h1>Create Blog</h1>
	<form onSubmit={this.submitForm}>
	  <label>
	    Title:
	    <input type="text" value={this.state.title}  onChange={this.handleChange('title')}/>
	  </label>
	  <label>
	    Content:
	    <input type="text" value={this.state.content}  onChange={this.handleChange('content')}/>
	  </label>
	  <input type="submit" value="Submit" />
        </form>
      </Layout>
    );
  }
}
export default BlogForm;
  • Отдельная страница просмотра блога
import Layout from '../../components/basicLayout';
import axios from 'axios';
import Router from 'next/router';
const Blog = props => (
  <Layout>
    <h1>{props.blog.title}</h1>
    <p>{props.blog.content}</p>
    <button onClick={deletePost(props.blog.blog_id)}>Delete</button>
  </Layout>
);
Blog.getInitialProps = async function(context) {
  const { id } = context.query;
  const res = await axios.get('<API route>'+ id , {headers: {"app_user_id":"test", "app_user_name": "test"}});
  const blog = await res.data.Item;
  return { blog };
};
const deletePost = function(id) {
    return function () {
      axios.delete('<API route>'+ id,{headers: {"app_user_id":"test", "app_user_name": "test"}})
      .then(response => {
	Router.push('/');
      });
    }
}
export default Blog;
  • Протестируйте представления локально, запустив `npm run dev`, он будет размещать представления на http: // localhost: 3000
  • Разверните Views с помощью Zeit прямо сейчас
  • Установить сейчас с помощью `npm install -g now`
  • Разверните, запустив команду `now` в корневом каталоге