0
1

More than 1 year has passed since last update.

【Express.JS×PostgreSQL】でテーブルのCRUD機能を実装する。

Last updated at Posted at 2023-01-16

Javascriptフレームワーク「Express.js」を使ってテーブルのCRUD機能を作ってみよう!

【最終目標】の画面はこんな感じの画面です。↓

2.png

【開発環境】
OS:Windows11
【ミドルウェア】
PostgreSQL
【Node.jsフレームワーク】
Express.js

準備

■【Node.js】のインストール
Node.jsは以下からインストール↓
Node.js

■PostgreSQLは以下からインストール↓
PostgreSQL

Express Generatorをインストールする。

ExpressのWebアプリケーションを開発するにあたって便利なソフトウェア「Express Generator」というものがあります。
まずは、このソフトをコマンドを使ってインストールしましょう。
コマンドオプションに「-g」を付けているのは、Node.jsが仁洙ルールされている環境全体で使えるように指定しています。

npm install -g express-generator

PostgreSQLでテーブルを作成

-- Table: public.mydata

-- DROP TABLE IF EXISTS public.mydata;

CREATE TABLE IF NOT EXISTS public.mydata
(
    id integer NOT NULL DEFAULT nextval('mydata_id_seq'::regclass),
    name character varying COLLATE pg_catalog."default" NOT NULL,
    mail character varying COLLATE pg_catalog."default",
    age integer,
    CONSTRAINT mydata_pkey PRIMARY KEY (id)
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.mydata
    OWNER to postgres;

プロジェクトの作成

さて、「Express Generator」をインストールしたところで、Expressのプロジェクトを作成してみましょう。
Windowsのコマンドプロンプトから以下のコマンドを入力してください。

express --view=ejs プロジェクト名

アプリケーションに必要なパッケージをインストールする。

プロジェクトを作成しましたが、実はもう一つやることがあります。それは、「開発に必要なパッケージをインストールする」作業です。
この作業をうっかり忘れてしますと、アプリケーションは作成できません。
というわけで、さっそくコマンド↓を使ってパッケージをインストールしましょう。

npm install

ちなみに「npm install」コマンドは開発に必要なパッケージをすべてインストールしてくれるので、開発者側で必要なパッケージをいちいち検索しなくてもいいんですね。
なので、この作業は絶対に忘れないようにしましょう!

アプリケーションを起動する。

さて、いよいよアプリケーションを起動してみましょう。起動する方法は、コマンドを入力して以下のURLをブラウザに打ち込むだけです。
【URL】
http://localhost:3000/

npm start

PostgreSQLのモジュールをインストールする。

PostgreSQLをアプリケーションで使うためには、モジュールをインストールする必要があります。
Windowsの「cd」コマンドを使ってプロジェクトのディレクトリに移動して以下のコマンドを入力しましょう。

npm install pg

プロジェクトの中身

さて、今回作成するプロジェクトの中はこのようなディレクトリ構造になっています。
1.png

今回使うのは、①Routes府フォルダの中にある「hello.js」と②Viewsフォルダの中にある「index.ejs」、「add.ejs」、「edit.ejs」、「delete.ejs」、「show.ejs」、「find.ejs」です。

①のhello.jsはブラウザから送られてきたデータをサーバで処理するファイルです。
②のそれぞれのファイルはサーバから返却された結果をブラウザに表示するためのテンプレートです。

まずは、hello.jsのルーティングが必要なので、app.jsにモジュールを追加しましょう。

必要なのは上から数えて6行目の「var hello=require('./routes/hello');」の部分です。ここにhello.js追加することで
Express.jsでhello.jsが使えるようにルーティングをします。

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var hello=require('./routes/hello');//2023.1.14追加

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
const session=require('express-session');//2023.1.14追加 sessionモジュールを追加

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

//2023.1.14追加
var session_opt={
  secret:'keyboard cat',
  resave:false,
  saveUninitialized:false,
  cookie:{maxAge:60*60*1000}
}
app.use(session(session_opt));
//2023.1.14追加

app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/hello',hello);//2023.1.14追記

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

最初に表示する画面「index.ejs」と処理を作成する。

最初に、「http://localhost:3000/index」へのアクセスしたときに表示する画面と処理を作成します。

index.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <table class="table">
                <% for(var i in content) { %>
                    <tr>
                        <% var obj = content[i]; %>
                        <th><%=obj.id %></th>
                        <td><%=obj.name %></td>
                        <td><%=obj.mail %></td>
                        <td><%=obj.age %></td>
                    </tr>
                <% } %>
            </table>
        </div>
    </body>
</html>

つづいて、index画面が表示されるための所処理を作成します。
★重要★
コードの最後にhello.jsのモジュールをエクスポートする「module.exports=router;」のコードを追加しておくことを忘れないでください。これを忘れると処理が正常に行われず、ブラウザにデータが表示されません。

hello.js
const express=require('express');
const router=express.Router();
//const postgresql = require('pg');//2023.1.14 postgreSQLパッケージをインポート
var pg = require('pg');//2023.1.14 データベースオブジェクトの取得

router.get('/', function(req, res, next) {
      var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
      database: 'expressDb', //PostgreSQLに作成したデータベース名
      user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
      password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
      host: 'localhost',
      port: 5432,
    });
 
    pool.connect( function(err, client) {
      if (err) {
        console.log(err);
      } else {
        client.query('SELECT * FROM mydata', function (err, result) {
          res.render('hello/index', {
            title: 'Express',
            content: result.rows,
          });
          console.log(result); //コンソール上での確認用なため、この1文は必須ではない。
        });
      }
    });
  });

新規登録する画面「index.ejs」と処理を作成する。

新規登録する画面と処理を作成します。

add.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <p><%= content %></p>
            <form method="post" action="/hello/add">
                <div class="form-group">
                    <label for="name">NAME</label>
                    <input type="text" name="name" id="name" class="form-control"/>
                </div>
                <div class="form-group">
                    <label for="mail">Mail</label>
                    <input type="text" name="mail" id="mail" class="form-control"/>
                </div>
                <div class="form-group">
                    <label for="age">Age</label>
                    <input type="number" name="age" id="age" class="form-control"/>
                </div>
                <input type="submit" value="作成" class="btn btn-primary"/>
            </form>
        </div>
    </body>
</html>
hello.js
//新規作成ページ「/hello/add」を表示する処理
router.get('/add',(req,res,next)=>{
  var data={
    title:'Hello/Add',
    content:'新しいレコードを入力'
  }
  res.render('hello/add',data);
});

//新規作成ページ「/hello/add」からデータベースに登録する処理
router.post('/add',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストで送信された各情報を変数に格納する。
  const inputName=req.body.name;
  const inputMail=req.body.mail;
  const inputAge=req.body.age;

  //INSERTクエリを作成する。※Value値は必ず「$1,$2,$3,...」とすること!(ほかの変数にすると構文エラーになる。)
  const text = 'INSERT INTO mydata(name, mail,age) VALUES($1, $2, $3) RETURNING *'
  //INSERTクエリのVALUE値に入れる各情報を配列で定義する。
  const values = [inputName, inputMail,inputAge]

  //データベースへの登録処理
  pool.connect( function(err, client) {
    try{
      //トランザクションを開始する。
      client.query("START TRANSACTION");
      //INSERTクエリを実行する。
      client.query(text, values);
      //登録処理をコミットする。
      client.query("COMMIT");
      
    }catch(err){
      client.rollback(); //登録が失敗したらロールバックする。
    }finally{
      client.release(); //データベースの接続を切る。
      res.redirect('/hello'); //「/hello」ページへリダイレクトする。
    }

  });
 
});

編集する画面「index.ejs」と処理を作成する。

編集する画面と処理を作成します。

edit.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <p><%= content %></p>
            <form method="post" action="/hello/edit">
                <input type="hidden" name="id" value="<%= mydata.id %>"/>
                <div class="form-group">
                    <label for="name">NAME</label>
                    <input type="text" name="name" id="name" class="form-control" value="<%= mydata.name %>"/>
                </div>
                <div class="form-group">
                    <label for="mail">Mail</label>
                    <input type="text" name="mail" id="mail" class="form-control" value="<%= mydata.mail %>"/>
                </div>
                <div class="form-group">
                    <label for="age">Age</label>
                    <input type="number" name="age" id="age" class="form-control" value="<%= mydata.age %>"/>
                </div>
                <input type="submit" value="更新" class="btn btn-primary"/>
            </form>
        </div>
    </body>
</html>

hello.js
//編集「edit.ejs」を表示する処理。
router.get('/edit',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });
  //リクエストパラメータからid値を取得する。
  const id= req.query.id;
  //SELECTクエリを生成する。
  const text= 'select * from mydata where id = $1';
  //where句にidをつめる。
  const values=id;
  
  pool.connect(function(err,client){
    try{
      client.query('START TRANSACTION');
      //クエリを実行する。第一引数:SELECTクエリを入力、第二引数:リクエストパラメータの配列を入力、第三引数:関数
      client.query(text,[values],function(err,result){
        res.render('hello/edit',{
          title:'hello/edit',
          content:'id='+id+'のレコードを編集する。',
          mydata:result.rows[0]
        });
      console.log(result);
      console.log("正常に処理完了。");
    });
      client.query('COMMIT');
  
    }catch(err){
      client.rollback();
    }finally{
      client.release();
    }
  });

});

//更新を実行する処理
router.post('/edit',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストで送信された各情報を変数に格納する。
  const inputId=req.body.id;
  const inputName=req.body.name;
  const inputMail=req.body.mail;
  const inputAge=req.body.age;

  //UPDATEクエリを生成する。
  const text='update mydata set name=$1,mail=$2,age=$3 where id=$4;';
  const values=[inputName,inputMail,inputAge,inputId];

  pool.connect(function(err,client){
    try{
      client.query('START TRANSACTION');
      client.query(text,values,function(err,result){
        console.log("正常に処理完了。");
      });
      client.query('COMMIT');
      //helloページにリダイレクト
      res.redirect('/hello');
    }catch(err){
      client.rollback();
    }finally{
      client.release();
    }
  });
});

削除画面を表示する「show.ejs」と処理を作成する。

delete.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <p><%= content %></p>
            <table class="table">
                <tr>
                    <th>NAME</th>
                    <td><%= mydata.name %></td>
                </tr>
                <tr>
                    <th>MAIL</th>
                    <td><%= mydata.mail %></td>
                </tr>
                <tr>
                    <th>AGE</th>
                    <td><%= mydata.age %></td>
                </tr>
            </table>
            <form method="post" action="/hello/delete">
                <input type="hidden" name="id" value="<%= mydata.id %>"/>
                <input type="submit" value="削除" class="btn btn-primary"/>
            </form>
        </div>
    </body>
</html>
hello.js
//削除ページ[delete.ejs]を表示する処理
router.get('/delete',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストパラメーターからid値を取得する。
  const id =req.query.id;
  //SELECTクエリを生成する。
  const text='select * from mydata where id = $1';
  const values =id;

  pool.connect(function(err,client){
    try{
      client.query('START TRANSACTION');
      client.query(text,[values],function(err,result){
        res.render('hello/delete',{
          title:'hello/delete',
          content:'id='+id+'のレコード',
          mydata:result.rows[0]
      });
      });
      client.query('COMMIT');
    }catch(err){
      client.rollback();
    }finally{
      client.release();
    }
  });

});

//登録データの削除処理
router.post('/delete',(req,res,next)=>{
  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストで送信された各情報を変数に格納する。
  const inputId=req.body.id;

  //DELETEクエリを生成する。
  const text='delete from mydata where id = $1';
  const values=inputId;

  pool.connect(function(err,client){
    try{
      client.query('START TRANSACTION');
      client.query(text,[values],function(err,result){
        console.log("削除処理完了");
      });
      client.query('COMMIT');
      //helloページへリダイレクトする。
      res.redirect('/hello');
    }catch(err){
      client.rollback();
    }finally{
      client.release();
    }
  });
});

詳細画面を表示する「show.ejs」と処理を作成する。

詳細画面を表示する画面とその機能を作成します。

show.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <p><%= content %></p>
            <table class="table">
                <tr>
                    <th>ID</th>
                    <td><%= mydata.id %></td>
                </tr>
                <tr>
                    <th>NAME</th>
                    <td><%= mydata.name %></td>
                </tr>
                <tr>
                    <th>MAIL</th>
                    <td><%= mydata.mail %></td>
                </tr>
                <tr>
                    <th>AGE</th>
                    <td><%= mydata.age %></td>
                </tr>
            </table>
        </div>
    </body>
</html>
hello.js
//詳細ページ「show.ejs」を実行する処理。
router.get('/show',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストパラメータからidを取得する。
  const id=req.query.id;
  //SELECTクエリを生成する。
  const text='select * from mydata where id = $1';
  //where句にidをつめる。
  const values=id;

  //データベースへの登録処理
  pool.connect( function(err, client) {
    try{
      //トランザクションを開始する。
      client.query("START TRANSACTION");
      //SELECTクエリを実行する。第一引数:SELECTクエリを入力、第二引数:リクエストパラメータの配列を入力、第三引数:関数
      client.query(text, [values], function (err, result) {
        res.render('hello/show', {
          title: 'Hello/Show',
          content:'id=のレコード',
          mydata: result.rows[0],//←取得したレコード「row」のIndex 0番目を指定する。
        });
        
        console.log(result); //コンソール上での確認用なため、この1文は必須ではない。
      });

      //コミットする。
      client.query("COMMIT");
      
    }catch(err){
      client.rollback(); //失敗したらロールバックする。
    }finally{
      client.release(); //データベースの接続を切る。
      
    }
  });

});

検索画面を表示する「find.ejs」と処理を作成する。

検索画面を表示する「find.ejs」と処理を作成します。

find.ejs
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8">
        <title><%=title%></title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <link rel="stylesheet" href="/public/stylesheets/style.css"/>
    </head>
    <body class="container">
        <header>
            <h1><%=title%></h1>
        </header>
        <div role="main">
            <p><%= content %></p>
            <form method="post" action="/hello/find">
                <div class="form-group">
                    <label for="find">FIND</label>
                    <input type="text" name="find" id="find" class="form-control" value="<%= find %>"/>
                </div>
                <input type="submit" value="更新" class="btn btn-primary"/>
            </form>
            <table class="table">
                <% for(var i in mydata) { %>
                    <tr>
                        <% var obj = mydata[i]; %>
                        <th><%=obj.id %></th>
                        <td><%=obj.name %></td>
                        <td><%=obj.mail %></td>
                        <td><%=obj.age %></td>
                    </tr>
                <% } %>
            </table>
        </div>
    </body>
</html>
hello.js
//検索「find」ページを表示する処理
router.get('/find',(req,res,next)=>{
  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  pool.connect( function(err, client) {
    if (err) {
      console.log(err);
    } else {
      client.query('SELECT * FROM mydata', function (err, result) {
        res.render('hello/find', {
          title: 'Express',
          find:'',
          content: '検索条件を入力してください。',
          mydata:result.rows
        });
        console.log(result); //コンソール上での確認用なため、この1文は必須ではない。
      });
    }
  });
});

//検索ページ「find.ejs」の検索フォームにテキストを入力して結果を取得する処理
router.post('/find',(req,res,next)=>{

  var pool = new pg.Pool({ //【★大事★】poolクラスをnewする。
    database: 'expressDb', //PostgreSQLに作成したデータベース名
    user: 'postgres', //ユーザー名はデフォルト以外を利用している人は適宜変更してください。
    password: 'postgres', //PASSWORDにはPostgreSQLをインストールした際に設定したパスワードを記述。
    host: 'localhost',
    port: 5432,
  });

  //リクエストパラメーター「find」から値を取得する。
  var findInputText = req.body.find;

  //リクエストパラメーターで取得した変数「findInputText」を追加したSELECTクエリを生成する。
  const text='select * from mydata where '+ findInputText;
  console.log(text);
  
  pool.connect(function(err,client){
    try{
      //トランザクションを開始する。
      client.query('START TRANSACTION');
      //クエリを実行する。第一引数にformから取得したテキストを追加したSELECTクエリ、第二引数に関数を追加。
      client.query(text,function(err,result){
        res.render('hello/find',{
          title:'hello/find',
          find:findInputText,
          content:'検索条件:'+findInputText+'のレコード',
          mydata:result.rows
        });
      });
      //処理をコミットする。
      client.query('COMMIT');
    }catch(err){
      client.rollback();
    }finally{
      client.release();
    }
  });

});

module.exports=router;

Node.js、Express.jsにおすすめな書籍と記事を紹介

■Node.js超入門 第3版 単行本 – 2020/7/18
著者:掌田 津耶乃
[Node.js超入門 第3版]3.png

■LogRopcket「CRUD REST API with Node.js, Express, and PostgreSQL」
4.png

以上です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1