在 Koa 服务中构建 RESTful api

2023年 7月 14日 73.5k 0

未命名文件 (2).jpg

新增

当前代码的 github 仓库地址 koa-tutorial

https://github.com/egolink0/koa-tutorial.git

导引

通过一步步的练习,最后你可以通过本文对以下方面有基本的了解。

  • 了解 RESTful api 规范
  • 了解 koa 如何构建 CRUD 接口
  • 了解 docker 如何起一个 mysql 服务
  • 了解 knex 如何在 koa 系统中与数据库连接

环境要求

在项目练手之前,你需要基本配置好下面这些

必要环境

  • nodejs
  • docker
  • docker compose

非必要

  • git
  • nodemon

最终所有包版本

"knex": "^2.4.2",
"koa": "^2.14.2",
"koa-bodyparser": "^4.4.0",
"koa-router": "^12.0.0",
"mysql2": "^3.3.5"

初始化服务入口

第一步,初始化项目的基本环境和安装包

git init
npm init -y
npm install koa@latest knex@latest mysql2@latest -S

之后,构建 src/server/index.js 文件,写入基本的入口配置

const Koa = require("koa");

const app = new Koa();

app.use(async (ctx) => {
  ctx.body = {
    status: 200,
    message: "hello world",
  };
});

const PORT = 4000;

const server = app.listen(PORT, () => {
  console.log(`Server is listening to ${PORT}`);
});

启服务

node ./src/server/index.js

现在,你可以通过浏览器打开 localhost:4000 页面,可以看到页面直接返回结果

{"status":200,"message":"hello world"}

之后就要开始配置数据库

配置数据库

1. 起 mysql 服务

这里,我们使用 mysql 作为对应的数据库。根据下面 yaml 配置 起一个 mysql 的服务
docker-compose -f ./xxx.yaml up -d

version: '3'
services:
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      MYSQL_DATABASE: koa_restful_api
      MYSQL_USER: koa # 数据库用户名
      MYSQL_PASSWORD: 123456 # koa 对应的用户名密码
    ports:
      - "3306:3306"
    volumes:
      - /data/mysql5.7:/var/lib/mysql 

此时查看 docker ps 可以看对应的容器,可以进入容器进行 mysql 的操作。

> docker ps  # 查看所有运行的容器
> docker exec -it 容器ID bash # 进入正在运行的容器
> mysql -u koa -p # 输入密码,进入 mysql 交互界面
> show databases; # sql

2. knex 配置连接

执行 knex init 初始化 knex 配置后,会在根目录下生成 knexfile.js文件。修改文件里面的配置,根据 docker 的 mysql 服务配置,修改 development 环境下的配置。

const path = require("path");

const BASE_PATH = path.join(__dirname, "src", "server", "db");

module.exports = {
  development: { // development
    client: "mysql2", 
    connection: "mysql://koa:123456@localhost:3306/koa_restful_api", // 根据自己的 mysql 配置修改成相应的
    migrations: {
      directory: path.join(BASE_PATH, "migrations"),
    },
    seeds: {
      directory: path.join(BASE_PATH, "seeds"),
    },
  },
}

配置 src/server/db/connection.js 数据库连接实例

const environment = process.env.NODE_ENV || 'development';
const configs = require('../../../knexfile.js');

const knex = require('knex')(configs[environment]);

module.exports = knex;

3. 初始化数据库表结构

运行 knex migrate:make movies,构建 migrations 初始化文件,修改里面的内容,用于构建相应的表结构

// src/server/db/migrations/xx_movies.js
// 构建表
exports.up = function (knex) {
  return knex.schema.createTable("movies", (table) => {
    table.increments();
    table.string("name").notNullable().unique();
    table.string("genre").notNullable();
    table.integer("rating").notNullable();
    table.boolean("explicit").notNullable();
  });
};

// 删除表
exports.down = function (knex) {
  return knex.schema.dropTable("movies");
};

指定 dev 环境,并运行构建命令: knex migrate:latest --env development
然后可以看到数据库表结构如下,movies 就生成了。

mysql> show tables; # 查看表 list
+---------------------------+
| Tables_in_koa_restful_api |
+---------------------------+
| knex_migrations           |
| knex_migrations_lock      |
| movies                    |
+---------------------------+
3 rows in set (0.00 sec)


mysql> desc movies; # 查看表结构
+----------+------------------+------+-----+---------+----------------+
| Field    | Type             | Null | Key | Default | Extra          |
+----------+------------------+------+-----+---------+----------------+
| id       | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| name     | varchar(255)     | NO   | UNI | NULL    |                |
| genre    | varchar(255)     | NO   |     | NULL    |                |
| rating   | int(11)          | NO   |     | NULL    |                |
| explicit | tinyint(1)       | NO   |     | NULL    |                |
+----------+------------------+------+-----+---------+----------------+
5 rows in set (0.00 sec)

4. 数据初始化

有了表,但是还没有数据,此时需要生成 seed 文件,可以初始化一些数据。通过执行 knex seed:make movies_seed 得到 movies_seed 文件,然后修改相应的文件。

// src/server/seeds/movies_seed.js
exports.seed = (knex, Promise) => {
  return knex("movies")
    .del()
    .then(() => {
      return knex("movies").insert({
        name: "A",
        genre: "Fantasy",
        rating: 7,
        explicit: false,
      });
    })
    .then(() => {
      return knex("movies").insert({
        name: "B",
        genre: "Science Fiction",
        rating: 9,
        explicit: true,
      });
    })
    .then(() => {
      return knex("movies").insert({
        name: "C",
        genre: "Action",
        rating: 5,
        explicit: false,
      });
    });
};

修改完成后,执行 seed 函数往数据库插入一些数据,初始化 knex seed:run --env development
此时,查看数据表,新增了三行新的数据。

mysql> select * from movies;
+----+------+-----------------+--------+----------+
| id | name | genre           | rating | explicit |
+----+------+-----------------+--------+----------+
|  1 | A    | Fantasy         |      7 |        0 |
|  2 | B    | Science Fiction |      9 |        1 |
|  3 | C    | Action          |      5 |        0 |
+----+------+-----------------+--------+----------+
3 rows in set (0.00 sec)

搭建路由

路由部分,使用 koa-router 包, 所以先安装对应的 npm 依赖 npm i koa-router@latest -S
RESTful api 规范包含了增删改查的一些原则,下面将开始构建相应的部分。

1. 根路由 /

初始化服务端入口文件 index.js 中 包含了对所有请求的统一返回值 hello world,但是实际项目中,会出现多个接口的情况,此时就涉及到路由的概念,先创建根地址的路由返回值。
构建 src/server/routes/index.js 文件存放根路由响应数据。

const Router = require("koa-router");

const router = new Router();

router.get("/", async (ctx) => {
  ctx.body = {
    status: "success",
    message: "hello, world!",
  };
});

module.exports = router;

并在入口文件处引入

const indexRoutes = require("../server/routes/index");

app.use(indexRoutes.routes());

再次访问 localhost:4000 得到和上述一样的 hello world JSON 数据

RESTful api 概览

项目需要构建的 api 规范结构包含以下几个部分

URL http method desc
/api/v1/movies get 获取 movies 列表
/api/v1/movies/:id get 获取一个 movie
/api/v1/movies post 增加一个 movie
/api/v1/movies/:id put 修改一个 movie
/api/v1/movies/:id delete 删除一个 movie

2. (查)列表 /api/v1/movies

在 src/server/db/queries/movies.js 创建 getMovies 查询数据库数据的部分

const knex = require("../connection");

function getAllMovies() {
  return knex("movies").select("*");
}

module.exports = {
  getAllMovies,
};

在 src/server/routes/movies.js 中创建 api 接口 get 路由部分


const Router = require("koa-router");
const queries = require("../db/queries/movies");

const router = new Router();
const BASE_URL = `/api/v1/movies`;

router.get(BASE_URL, async (ctx) => {
  try {
    const movies = await queries.getAllMovies();
    ctx.body = {
      status: "success",
      data: movies,
    };
  } catch (err) {
    console.log(err);
  }
});

module.exports = router;

在 src/server/index.js 入口文件处,引入 movies 的路由部分

const movieRoutes = require("../server/routes/movies"); // movies

app.use(movieRoutes.routes()); // movies route

重启服务,此时在浏览器或者 postman 构建请求 localhost:4000/api/v1/movies 可以得到下面的列表信息

{  
    "status":"success",  
    "data":[  
        {  
            "id":1,  
            "name":"A",  
            "genre":"Fantasy",  
            "rating":7,  
            "explicit":0  
        },  
        {  
            "id":2,  
            "name":"B",  
            "genre":"Science Fiction",  
            "rating":9,  
            "explicit":1  
        },  
        {  
            "id":3,  
            "name":"C",  
            "genre":"Action",  
            "rating":5,  
            "explicit":0  
        }  
    ]  
}

3. (查)某一个电影的详细数据 /api/v1/movies/:id

和上面的步骤一致,先创建 query 方法

function getSingleMovie(id) {
  return knex("movies")
    .select("*")
    .where({ id: parseInt(id) });
}

再构建路由部分

router.get(`/api/v1/movies/:id`, async (ctx) => {
  try {
    const movie = await queries.getSingleMovie(ctx.params.id);
    if (movie.length) {
      ctx.body = {
        status: "success",
        data: movie,
      };
    } else {
      ctx.status = 404;
      ctx.body = {
        status: "error",
        message: "That movie does not exist.",
      };
    }
  } catch (err) {
    console.log(err);
  }
});

此时访问浏览地址 //localhost:4000/api/v1/movies/1 可以得到 id 为 1 的电影的数据

{
	"status": "success",
	"data": [{
		"id": 1,
		"name": "A",
		"genre": "Fantasy",
		"rating": 7,
		"explicit": 0
	}]
}

4. (增加)一个电影 post

query 部分

function addMovie(movie) {
  return knex("movies").insert(movie); // 返回 [id]
}

路由部分

router.post(`/api/v1/movies`, async (ctx) => {
  try {
    const movie = await queries.addMovie(ctx.request.body); // koa-bodyparser
    if (movie.length) { // 根据数据库返回值处理
      ctx.status = 201;
      ctx.body = {
        status: "success",
        data: { id: movie[0] },
      };
    } else {
      ctx.status = 400;
      ctx.body = {
        status: "error",
        message: "Something went wrong.",
      };
    }
  } catch (err) {
    ctx.status = 400;
    ctx.body = {
      status: "error",
      message: err.message || "Sorry, an error has occurred.",
    };
  }
});

使用到了 request 的 body 部分,需要增加解析插件在服务入口 npm i koa-bodyparser@latest -S

const koa = require("koa");
const bodyParser = require("koa-bodyparser"); // koa-bodyparser

const app = new koa();
const PORT = 4000;

const indexRoutes = require("../server/routes/index");
const moviesRoutes = require("../server/routes/movies");

app.use(bodyParser()); // 在路由之前加入中间件
app.use(indexRoutes.routes());
app.use(moviesRoutes.routes());

此时,在 postman 中发送一个 post 请求后,数据库增加了一行,返回了新的一行的 id: 4

image.png

5. (改)一个电影信息

query 的 update 部分

function updateMovie(id, movie) {
  return knex("movies")
    .update(movie)
    .where({ id: parseInt(id) });
}

路由部分,使用 put 更新

router.put(`/api/v1/movies/:id`, async (ctx) => {
  try {
    const movie = await queries.updateMovie(ctx.params.id, ctx.request.body);
    if (movie) {
      ctx.status = 200;
      ctx.body = {
        status: "success",
        data: { id: movie },
      };
    } else {
      ctx.status = 404;
      ctx.body = {
        status: "error",
        message: "That movie does not exist.",
      };
    }
  } catch (err) {
    ctx.status = 400;
    ctx.body = {
      status: "error",
      message: err.message || "Sorry, an error has occurred.",
    };
  }
});

用 postman 测试,修改 id 为 1 的数据的 name

image.png

查看对应的数据库, 第一条数据名称已经更改为 D11

6. (删)一个电影

query 部分

function deleteMovie(id) {
  return knex("movies")
    .del()
    .where({ id: parseInt(id) });
}

路由部分,delete

router.delete(`/api/v1/movies/:id`, async (ctx) => {
  try {
    const movie = await queries.deleteMovie(ctx.params.id);
    if (movie) {
      ctx.status = 200;
      ctx.body = {
        status: "success",
        data: { id: movie }, // 返回 id
      };
    } else {
      ctx.status = 404;
      ctx.body = {
        status: "error",
        message: "That movie does not exist.",
      };
    }
  } catch (err) {
    ctx.status = 400;
    ctx.body = {
      status: "error",
      message: err.message || "Sorry, an error has occurred.",
    };
  }
});

删除 id 为 1 的数据

image.png

查看数据,id=1 的数据已经被删除了

mysql> select * from movies;
+----+------+-----------------+--------+----------+
| id | name | genre           | rating | explicit |
+----+------+-----------------+--------+----------+
|  2 | B    | Science Fiction |      9 |        1 |
|  3 | C    | Action          |      5 |        0 |
|  4 | D    | D               |      9 |        5 |
+----+------+-----------------+--------+----------+
3 rows in set (0.00 sec)

结束

至此,阅读本文的你,应该已经掌握了在 koa 系统中从 0 到 1 搭建一个 RESTful api 规范的接口流程,下一步,你可以增加 TDD 测试的部分,了解测试驱动开发的流程。

参考

  • Building a RESTful API with Koa and Postgres

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论