Building an Online Menu using Apostrophe Headless + Nuxt / Nginx: Part 1

In this tutorial, we'll demonstrate how to use Apostrophe Headless with Nuxt for the frontend and Nginx as a reverse-proxy.

apostrophe nuxt part 1 banner

In this tutorial, we'll demonstrate how to use Apostrophe Headless with Nuxt for the frontend and Nginx as a reverse-proxy, as well as optional Docker instructions for Docker users. We'll be creating an online storefront for a restaurant that will allow customers to register and place an order. Let's dive in.


If not using Docker, the following packages and applications are prerequisites for this tutorial, which has been written with macOS users in mind. If you're starting fresh, we strongly recommend using Homebrew.

  • Git: brew install git
  • Node / npm: brew install node
  • MongoDB: brew tap mongodb/brew && brew install mongodb-community && brew services start mongodb-community
  • ImageMagick: brew install imagemagick

Getting Started

Start by cloning the project into your preferred directory using the terminal.

git clone && cd apos-nuxt-demo

Next, we'll walk through the process of running the application, with and without Docker.

Without Docker

  1. In your terminal, run npm run install-app to install the application.
  2. Next, run cd backend && node app fixtures:all && cd .. install the project fixtures.
  3. When complete, run npm run dev to start the developer environment.

The application will run at http://localhost:3333 and http://localhost:1337/cms for the front-end and back-end respectively. If you run into any issues, be sure to check that you have the prerequisites covered above installed correctly.

ūüí°If you prefer having separate logs for the front-end and back-end, you can do so by running the following commands for front-end and back-end respectively in separate terminal windows.

cd frontend && npm run dev

cd backend && npm run dev

With Docker

  1. In your terminal, run make.
  2. Run docker-compose exec demo-backend node app fixtures:all to install the project fixtures.
  3. When launched, the front-end will run at http://localhost and the back-end will run at http://localhost/cms.

If you prefer having separate logs for the front-end and back-end, run make logs-back and make logs-front in separate terminal windows.


The docker-compose.yml file describes the various containers and how they are organized:

  1. demo-db container for the MongoDB image.
  2. demo-backend container for Apostrophe, using MongoDB.
  3. demo-frontend container for Nuxt, contacting the backend on http://demo-backend:1337/cms.
  4. demo-reverse-proxy container for Nginx.


The reverse-proxy/local.conf declares how requests are dispatched:

‚Äčlisten 80;
server_name localhost;
root /usr/share/nginx/frontend;

location / {
  proxy_pass http://demo-frontend:3333;

location /cms/ {
  proxy_pass http://demo-backend:1337;

Everything on port localhost:80 will be redirected to Nuxt, except for urls pointing to /cms, which are redirected to Apostrophe.

In its configuration, Apostrophe has a matching prefix:

// in backend/app.js
prefix: '/cms',

This way, with Docker, you can access the frontend on http://localhost and the backend on http://localhost/cms.

Setting up Shop

Now that we're successfully running the application with the fixtures installed, let's take a look at the rendered project in the browser.

ūüí°You can navigate to¬†backend/lib/modules/fixtures/index.js¬†to explore how Apostrophe fetches and inserts documents into MongoDB.


  1. If you're using Docker, navigate to http://localhost/cms/login in your browser. Otherwise, navigate to http://localhost:1337/cms/login.
  2. The credentials for both user and password are admin.
admin bar for ApostropheCMS v2


You can click on "Menu Items" to look at the generated products by the "fixtures" step.

manage products in CMS apostrophe version 2



If you're using Docker, navigate to http://localhost in your browser. Otherwise, navigate to http://localhost:3333. If you follow the steps above, you should see an index of Menu Items.

restaurant dishes website menu


Looks delicious, but how does this work?

The Menu Item Schema

On the back-end, apostrophe-headless has been installed, and the menu-item module has been declared as a headless module with the restApi option set to true, automatically exposing a REST route at /api/v1/menu-items.

// backend/lib/modules/menu-items/index.js
module.exports = {
  extend: 'apostrophe-pieces',
  name: 'menu-item',
  alias: 'menuItem',
  restApi: true, // /api/v1/menu-items

Fetching the Menu

On the front-end, the index page fetches the menu items on this route, and Nuxt declares pages in the pages folder. In the asyncData method in this component, the following GET request is made:

‚Äč// frontend/pages/index.vue
const { results } = await $axios.$get('/api/v1/menu-items')

Component Template

The menu items are displayed in the component's template section with a standard v-for loop. As you can see, Apostrophe is automatically processing the images in the one-third format.

‚Äč// frontend/pages/index.vue
  <section class="home">
    <div class="home-menu-items">
      <div v-for="item in menuItems" :key="item._id" class="home-menu-items__item">
        <img :src="item.image._urls['one-third']" />
        <h3>{{ item.title }}</h3>
        <span>{{ item.description }}</span>

Customizing the Home Page

While everything above is a great starting point, we'll want customize to the home page to facilitate our brand messages or provide customers with additional information. You could edit the Vue component to add some additional static text, but this is a CMS, so let's use it to add an area of CMS managed content to keep content editors happy. For that task, the fixtures have already configured the page type "home" in the apostrophe-pages module:

‚Äč// backend/lib/modules/apostrophe-pages/index.js
module.exports = {
  restApi: true,
  apiTemplates: ['front-home'],
  types: [
      name: 'front-home',
      label: 'Front Home',

The restApi option is set to true, and we define a single page type for an Apostrophe editor to create: front-home. With apiTemplates: ['front-home'] we indicate to expose the rendered template.

Let's create a home page by navigating to the admin in our browser and selecting "Pages" in the Apostrophe admin bar. From there, we can select "New Page" and begin filling out the requisite fields.

new page modal apostrophe version 2


For our purposes, we're only focused on the type field: "Front Home". Choose "Save", and Apostrophe will redirect you to the newly created page. You can close the admin bar by clicking the Apostrophe logo on the far-left.

Adding Content

Now that we've added a home page, we're ready to start customizing. Let's start by adding Rich Text, to give our Menu a quick introduction. When hovering over the page, you should now see a small "+" button. Clicking this will display the available Widgets for the page. Select "Rich Text".

add content widget cms apostrophe version 2


You'll be presented with a rich text editor. Type anything you want, for example "Fresh food and delicious ingredients" and click out of the area. Now you can go to the front-end and reload the page (http://localhost on Docker, http://localhost:3333 otherwise) to see the changes.

in context welcome restaurant menu in CMS


How does this work?

In our index.vue component in frontend/pages/, the asyncData method fetches the pages exposed by Apostrophe and finds the home page we created.

Adding a Login

On the back-end, hovering just below the rich text area, a green bar will appear and you can add another widget. This time, choose "Link". Below are suggested settings (if you are not using Docker, be sure to type http://localhost:3333/login in the url field):

login CMS apostrophe version 2


The button is visible on the front-end after a refresh.

restaurant order preview in CMS


You can click the "Login" button and it will lead you to the login page.


Next Steps

In Part 2 of this tutorial, we'll create a customer and order some food! We'll be releasing this next week, but if you can't wait to dig in, see the complete documentation on Github. Looking for additional guidance or want to show off your application of Apostrophe Headless? Join the discussion in Discord and read our full documentation.

We are continually inspired by the work of our community, and we can't wait to see what you build next.