Launch Nuxt.js + Rails API on Docker and try CRUD operation

This is my first time touching Nuxt.js. I thought I'd make a Todo app, but I thought I'd hit the API, so I prepared the server side as well.

The server side is Ruby on Rails (API), the client side is Nuxt.ts (Nuxt.js + TypeScript), and the DB is postgres.

Regarding the environment construction, both server side/client side are running on Docker, and the directory structure is summarized in monolithic.

↓ Click here for the source code Kazuhiro-Mimaki/nuxt-rails-crud - GitHub

Operating environment

macOS Catalina : version 10.15.4 It is assumed that Docker for mac is already installed.

Directory structure

Directory structure


.
├── client-side
├── server-side
└── docker-compose.yml

1. Server side (Ruby on Rails)

Dockerfile creation

Create dockerfile under server-side /.

Dockerfile


FROM ruby:2.7.0

RUN apt-get update -qq && \
  apt-get install -y \
  build-essential \
  libpq-dev \
  nodejs \
  postgresql-client

WORKDIR /app

COPY Gemfil Gemfile.lock /app/
RUN bundle install

Create Gemfile, Gemfile.lock

Similarly, create Gemfile and Gemfile.lock under server-side /.

Describe the following in the Gemfile.

Gemfile


source 'https://rubygems.org'
gem 'rails', '6.0.3'

You can leave Gemfile.lock empty.

Create docker-compose.yml

I will write the rails and postgres settings in docker-compose.yml.

docker-compose.yml


version: '3.8'

volumes:
  db_data:

  services:
    db:
      image: postgres
      volumes:
        - db_data/var/lib/postgresql/data
      environment:
        POSTGRES_PASSWORD: password

    server-side:
      build: ./server-side/
      command: bundle exec rails server -b 0.0.0.0
      image: server-side
      ports:
        - 3000:3000
      volumes:
        - ./server-side:/server-app
      tty: true
      stdin_open: true
      depends_on:
        - db
      links:
        - db

rails new in API mode

If you hit the following command, rails related files will be created under server-side /.

$ docker-compose run server-side rails new . --api --force --database=postgresql --skip-bundle

Modify the contents of database.yml

If this is left as it is, the DB container cannot be accessed from the server-side container, so modify the contents of database.yml.

I think it looks like this

database.yml


default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Edit as follows.

database.yml


default: &default
  adapter: postgresql
  encoding: unicode
  host: db
  user: postgres
  password: password
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

Fixed to accept server-side host

With this setting, you can access server-side from Nuxt.

server-side/config/environments/development.rb


config.hosts << "server-side"

Create DB

Create a db by hitting the following command.

$ docker-compose run server-side rails db:create

Try to make it work

Access localhost: 3000 by typing the following command. If the rails default screen is displayed, it's OK!

$ docker-compose up -d

Implement server-side API

Hit the following command to enter the container and proceed with the work.

$ docker exec -it server-side bash

Set the routing.

routes.rb


Rails.application.routes.draw do
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
  namespace :api do
    namespace :v1 do
      resources :todos do
        collection do
          get :complete
        end
      end
    end
  end
end

Create Todo model, todos controller.

$ rails g model Todo title:string isDone:boolean
$ rails db:migrate
$ rails g controller api::v1::todos

The contents of the controller are written as follows.

api/app/controllers/api/v1/posts_controller.rb


class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: [:update, :destroy]

  def index
    todos = Todo.where(isDone: false)
    render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
  end

  def complete
    todos = Todo.where(isDone: true)
    render json: { status: 'SUCCESS', message: 'Loaded todos', data: todos }
  end

  def create
    todo = Todo.new(todo_params)
    if todo.save
      render json: { status: 'SUCCESS', data: todo }
    else
      render json: { status: 'ERROR', data: todo.errors }
    end
  end

  def destroy
    @todo.destroy
    render json: { status: 'SUCCESS', message: 'Deleted the todo', data: @todo }
  end

  def update
    if @todo.update(todo_params)
      render json: { status: 'SUCCESS', message: 'Updated the todo', data: @todo }
    else
      render json: { status: 'ERROR', message: 'Not updated', data: @todo.errors }
    end
  end

  private

    def set_todo
      @todo = Todo.find(params[:id])
    end

    def todo_params
      params.require(:todo).permit(:title, :isDone)
    end
end

Operation check

Refer to This article to check if CRUD operations can be performed using Postman. You can also check it with the curl command, but Postman is probably easier.

2. Client side (Nuxt.js)

Environment

Basically, just follow the Official Installation. It is assumed that node is already installed. (In this environment, LTS ver. 14.15.1 as of 12/15 is used.)

Creating a project

First, let's create a template with create-nuxt-app.

$ npx create-nuxt-app client-side

I think you will be asked various questions, but this time I set it as follows. (Others are default)

terminal


? Project name: client-side
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: Axios
? Linting tools: None
? Testing framework: None
? Rendering mode: Single Page App
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: None

Please set the settings around here to your liking. All options can be found here (https://github.com/nuxt/create-nuxt-app/blob/master/README.md).

Dockerfile creation

Create a Dockerfile under client-side /.

Dockerfile


FROM node:14.15.1

WORKDIR /client-app

COPY package.json yarn.lock ./

RUN yarn install

CMD ["yarn", "dev"]

Added client-side setting to docker-compose.yml

Add the client-side setting to docker-compose.yml that describes the server-side setting.

docker-compose.yml


version: '3.8'

volumes:
  db_data:

services:
  db:
    image: postgres
    volumes:
      - db_data/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password

  server-side:
    build: ./server-side/
    image: server-side
    ports:
      - 3000:3000
    volumes:
      - ./server-side:/server-app
    command: bundle exec rails server -b 0.0.0.0
    tty: true
    stdin_open: true
    depends_on:
      - db
    links:
      - db

  #Add below from here
  client-side:
    build: ./client-side/
    image: client-side
    ports:
      - 8000:8000
    volumes:
      - ./client-side:/client-app
      - /client-app/node_modules
    command: sh -c "yarn && yarn dev"

port setting

If this is left as it is, an error will occur, so set port and host as follows.

nuxt.config.js


export default {
  // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
  ssr: false,

  //Add here
  server: {
    port: 8000,
    host: '0.0.0.0',
  },

//Omitted below
}

Try to make it work

Type the following command to access localhost: 8000 and the Nuxt.js default screen will be displayed.

$ docker-compose up -d

This completes the environment construction!

3. Server-side and client-side cooperation

Finally, we will hit the server-side API from the client side. Impressive moment. .. ..

Fixed CORS (Cross-origin Resource Sharing) problem

For CORS, this article will be helpful. There was a solution in Official and README on GitHub. Install @ nuxtjs/proxy referring to the description in the README, and edit app/nuxt.config.js as follows. Since the server side port number was specified as 3000, here is server-side: 3000. (Communication between containers is resolved by the container name, so it is set to server-side instead of localhost.)

$ yarn add @nuxtjs/proxy

app/nuxt.config.js


modules: [
  '@nuxtjs/axios',
  '@nuxtjs/proxy'
],
//Add the following
proxy: {
  '/api': {
    target: 'http://server-side:3000',
    pathRewrite: {
      '^/api': '/api/v1/',
    },
  },
},

Set up Composition API and axios

I set it because I want to use it around here, but CRUD operation is possible without it.

shell


$ yarn add @nuxtjs/composition-api

client-side/nuxt.config.js


modules: [
  '@nuxtjs/proxy',
  //add to
  '@nuxtjs/axios',
  '@nuxtjs/composition-api',
],

client-side/tsconfig.json


"types": [
  "@types/node",
  "@nuxt/types",
  #add to
  "@nuxtjs/axios"
]

Type definition

Create a new models/todo.ts directory in client-side and write the following.

todo.ts


export interface ITodo {
  id: number;
  title: string;
  isDone: boolean;
}

Describe view

Actually, it should be divided into components and written, but this time I thought it would be easier to see if it was put together in one file, so I will put it together.

Describe the following contents in client-side/pages/index.vue.

client-side/pages/index.vue


<script lang="ts">
import {
  defineComponent,
  reactive,
  ref,
  onMounted,
} from "@nuxtjs/composition-api";
import { ITodo } from "../models/todo";
import $axios from "@nuxtjs/axios";

export default defineComponent({
  setup(_, { root }) {
    onMounted(() => {
      getTodo();
    });

    const todoItem = reactive({
      title: "",
      isDone: false,
    });

    const todoList = ref<ITodo[]>([]);
    const completeTodoList = ref<ITodo[]>([]);

    //post to do
    const addTodo = async () => {
      try {
        await root.$axios.post("/api/todos/", {
          title: todoItem.title,
          isDone: todoItem.isDone,
        });
        getTodo();
        todoItem.title = "";
      } catch (e) {
        console.log(e);
      }
    };

    //get todo
    const getTodo = async () => {
      try {
        const response = await root.$axios.get("/api/todos");
        todoList.value = { ...response.data.data };
        getCompleteTodo();
      } catch (e) {
        console.log(e);
      }
    };

    //update todo
    const updateTodo = async (i: number, todo: ITodo) => {
      try {
        const newTodo = todoList.value[i].title;
        await root.$axios.patch(`/api/todos/${todo.id}`, { title: newTodo });
      } catch (e) {
        console.log(e);
      }
    };

    //delete todo
    const deleteTodo = async (id: number) => {
      try {
        await root.$axios.delete(`/api/todos/${id}`);
        getTodo();
      } catch (e) {
        console.log(e);
      }
    };

    //to do done
    const completeTodo = async (todo: ITodo) => {
      try {
        todo.isDone = !todo.isDone;
        await root.$axios.patch(`/api/todos/${todo.id}`, {
          isDone: todo.isDone,
        });
        getTodo();
      } catch (e) {
        console.log(e);
      }
    };

    // complete_get todo
    const getCompleteTodo = async () => {
      try {
        const response = await root.$axios.get("/api/todos/complete");
        completeTodoList.value = { ...response.data.data };
      } catch (e) {
        console.log(e);
      }
    };

    return {
      todoItem,
      todoList,
      completeTodoList,
      addTodo,
      deleteTodo,
      updateTodo,
      completeTodo,
    };
  },
});
</script>

<template>
  <div class="container">
    <section class="todo-new">
      <h1>Add todos</h1>
      <input v-model="todoItem.title" type="text" placeholder="Fill in todo" />
      <button @click="addTodo()">Add Todo</button>
    </section>

    <section class="todo-index">
      <h1>Incomplete todos</h1>
      <ul>
        <li v-for="(todo, i) in todoList" :key="i">
          <input
            class="item"
            type="checkbox"
            :checked="todo.isDone"
            @change="completeTodo(todo)"
          />
          <input
            class="item"
            type="text"
            v-model="todo.title"
            @change="updateTodo(i, todo)"
          />
          <button @click="deleteTodo(todo.id)">delete</button>
        </li>
      </ul>
    </section>

    <section class="todo-complete">
      <h1>Complete todos</h1>
      <ul>
        <li v-for="(todo, i) in completeTodoList" :key="i">
          <input
            class="item"
            type="checkbox"
            :checked="todo.isDone"
            @change="completeTodo(todo)"
          />
          {{ todo.title }}
          <button @click="deleteTodo(todo.id)">delete</button>
        </li>
      </ul>
    </section>
  </div>
</template>

<style>
.container {
  margin: 80px auto;
  min-height: 100vh;
  text-align: center;
}

section {
  margin-bottom: 30px;
}

.item {
  font-size: 1rem;
  margin: 0 10x;
}

li {
  list-style: none;
  margin-bottom: 0.5em;
}
</style>

Let's actually operate

If you do docker-compose up and access localhost: 8000, you will see the following screen.

スクリーンショット 2020-12-09 10.36.16.png

Try adding/editing/deleting todo.

Summary

It was my first time to write a Dockerfile from scratch, so it was a good study. I have nothing to know about Nuxt.js, so I will study it. If you have any questions such as "The code here should be more like this!", Please give me some advice.

Recommended Posts

Launch Nuxt.js + Rails API on Docker and try CRUD operation
Run Docker environment Rails MySQL on Heroku. devise and hiding the twitter API
Launch Rails on EC2
Build Rails (API) x MySQL x Nuxt.js environment with Docker
Place "Create a to-do list using Rails API mode and React Hooks" on docker
Try Docker on Windows 10 Home
[Ruby on Rails] Read try (: [],: key)
Try using Redmine on Mac docker
Launch Rails on EC2 (manual deployment)
Rails on Docker environment construction procedure
CRUD features and MVC in Rails
Try Docker on Windows Home (September 2020)
Deploy Rails on Docker to heroku
Easily try C # 9 (.NET 5) on Docker
[Docker] How to create a virtual environment for Rails and Nuxt.js apps
[Rails API x Docker] Easy environment construction with shell & operation check with Flutter
Ruby on Rails ✕ Docker ✕ MySQL Introducing Docker and docker-compose to apps under development
Install docker and docker-compose on Alpine Linux
[Ruby on Rails] 1 model CRUD (Routing Main)
Building Rails 6 and PostgreSQL environment with Docker
Launch docker container on EC2 (personal memorandum)
Try using the Rails API (zip code)
Try deploying a Rails app on EC2-Part 1-
Rails migration column changes and so on.
Notes on Java's Stream API and SQL
Try putting Docker in ubuntu on WSL
Try the Docker environment on AWS ECS