DEV Community

Francisco Martin
Francisco Martin

Posted on

Nx: How to setup a full-stack app with Nx

NxAwesomeTodos

Awesome project to learn how to setup a full-stack project with Nx

Tech stack

  • TypeScript
  • Next.js
  • Express.js
  • React components
  • Storybook
  • Cypress

Considerations

This stack is only with demostration purposes. You can choose other techs like React, Angular or Vue. I know that the tests for client app are broken because we need to mock fetch API. I skipt this step because is out of the scope of this guide. Other steps missing would be setup a database, but those are "app" specific problem. The support for Cypress is out of the box so it is not necessary any extra configuration. Any improvement or correction will be appreciate.

Setup

Create the project

yarn create nx-workspace
Enter fullscreen mode Exit fullscreen mode

Answer this to questions

  • Workspace name > nx-awesome-todos
  • What to create in the new workspace > express
  • Application name > api
  • Use Nx Cloud? > No

Setup API project

Create file apps/api/src/app/index.ts and paste the following content

import * as express from 'express';

const todos = [
  {
    id: 1,
    content: 'First todo',
    completed: false,
  },
  {
    id: 2,
    content: 'Second todo',
    completed: true,
  },
];

const app = express();

app.get('/api/todos', (req, res) => {
  res.send({ todos });
});

export default app;
Enter fullscreen mode Exit fullscreen mode

Replace the content of apps/api/src/main.ts with

import app from './app';

const port = process.env.port || 3333;

const server = app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}/api`);
});

server.on('error', console.error);
Enter fullscreen mode Exit fullscreen mode

Run server

nx serve api
Enter fullscreen mode Exit fullscreen mode

Check that everything is working making a GET request to http://localhost:3333/api/todos

Setup API tests

Install supertest

yarn add supertest
Enter fullscreen mode Exit fullscreen mode

Create the first test at apps/api/test/api.test.ts

import * as supertest from 'supertest';
import app from '../src/app';

const requestWithSupertest = supertest(app);

describe('Example describe', () => {
  test('Example test', () => {
    expect(1).toBe(1);
  });
});

describe('Generic endpoints', () => {
  test('GET /api/todos', async () => {
    const res = await requestWithSupertest.get('/api/todos');
    expect(res.status).toEqual(200);
  });
});
Enter fullscreen mode Exit fullscreen mode

Test the app

nx test api
Enter fullscreen mode Exit fullscreen mode

Setup common library

nx g @nrwl/node:lib shared-types
Enter fullscreen mode Exit fullscreen mode

Add an interface at libs/shared-types/src/lib/shared-types.ts

export interface Todo {
  id: number;
  content: string;
  completed: boolean;
}
Enter fullscreen mode Exit fullscreen mode

Import the interface at apps/api/src/app/index.ts

import { Todo } from '@nx-awesome-todos/shared-types';
import * as express from 'express';

const todos: Todo[] = [
  // Rest of the code
];
Enter fullscreen mode Exit fullscreen mode

Setup client

This command create the app and the asociate Cypress setup

 nx g @nrwl/next:app client
Enter fullscreen mode Exit fullscreen mode

Setup CORS

Enable CORS at server

yarn add cors
yarn add -D @types/cors
Enter fullscreen mode Exit fullscreen mode

Setup cors at server apps/api/src/main.ts

import * as cors from 'cors';

app.use(cors());
Enter fullscreen mode Exit fullscreen mode

Get data from server

Create a file at apps/client/pages/api/todos.ts with this content

import type { NextApiRequest, NextApiResponse } from 'next';
import { Todo } from '@nx-awesome-todos/shared-types';

type Data = Todo[];

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const response = await fetch('http://localhost:3333/api/todos');
  const todos: Todo[] = await response.json();
  res.status(200).json(todos);
}
Enter fullscreen mode Exit fullscreen mode

Change the content of apps/client/pages/index.tsx

import { useEffect, useState } from 'react';
import { Todo } from '@nx-awesome-todos/shared-types';
import styles from './index.module.scss';

export function Index() {
  const [todos, setTodos] = useState<Todo[]>([]);

  useEffect(() => {
    fetch('/api/todos')
      .then((res) => res.json())
      .then((res) => setTodos(res.todos));
  }, []);

  return (
    <div className={styles.page}>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.content} - {todo.completed ? 'completed' : 'not completed'}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default Index;
Enter fullscreen mode Exit fullscreen mode

Setup client styles

Creat global variables and styles. Create a folder named styles and a file named variables.scss

$test-gray: gray;
Enter fullscreen mode Exit fullscreen mode

Load style variables

// eslint-disable-next-line @typescript-eslint/no-var-requires
const withNx = require('@nrwl/next/plugins/with-nx');
const path = require('path');

/**
 * @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
 **/
const nextConfig = {
  nx: {
    // Set this to true if you would like to to use SVGR
    // See: https://github.com/gregberge/svgr
    svgr: false,
  },
  reactStrictMode: true,
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
    prependData: `@import "variables.scss";`,
  },
  images: {
    domains: ['images.unsplash.com'],
  },
};

module.exports = withNx(nextConfig);
Enter fullscreen mode Exit fullscreen mode

Restart the development server for the changes to take effect.

Setup client eslint

yarn add -D prettier eslint-plugin-prettier
Enter fullscreen mode Exit fullscreen mode

Add prettier plugin to root .eslintrc.json. To run linter

nx lint client
Enter fullscreen mode Exit fullscreen mode

Setup client stylelint

Install dependencies

yarn add -D stylelint nx-stylelint
Enter fullscreen mode Exit fullscreen mode

Run setup

nx g nx-stylelint:configuration --project client
nx g nx-stylelint:scss --project client
Enter fullscreen mode Exit fullscreen mode

Add your rules to .stylelintrc.json.

Run linter

nx stylelint client
Enter fullscreen mode Exit fullscreen mode

Setup client tests

Install dependencies

yarn add -D @testing-library/jest-dom
Enter fullscreen mode Exit fullscreen mode

Create two files:

apps/client/__mocks__/fileMock.js

module.exports = 'test-file-stub';
Enter fullscreen mode Exit fullscreen mode

apps/client/__mocks__/styleMock.js

module.exports = {};
Enter fullscreen mode Exit fullscreen mode

Add this two lines inside apps/client/jest.config.js

testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
Enter fullscreen mode Exit fullscreen mode

Create file named apps/client/jest.setup.js with this content

import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
Enter fullscreen mode Exit fullscreen mode

Create the folder for tests named apps/client/__tests__. Place your tests here. You can create subfolders if you need to keep your tests organized.

Run your client tests

nx test client
Enter fullscreen mode Exit fullscreen mode

Note that you should mock the fetch request in order to tests works. If your project is large (and probably is because your are setting up a monorepo) you can considerer use axios and moxios to handle network requests.

Create component library

nx g @nrwl/react:lib ui
Enter fullscreen mode Exit fullscreen mode

Create component

You can create a component manually or using a command like this

nx g @nrwl/react:component todos --project=ui --export
Enter fullscreen mode Exit fullscreen mode

Copy and paste the following content in the created component

import { Todo } from '@nx-awesome-todos/shared-types';

export interface TodosProps {
  todos: Todo[];
}

export function Todos(props: TodosProps) {
  return (
    <ul>
      {props.todos.map((t) => (
        <li className={'todo'} key={t.id}>
          {t.content}
        </li>
      ))}
    </ul>
  );
}

export default Todos;
Enter fullscreen mode Exit fullscreen mode

Use the new component in the client project

import { Todos, Ui } from '@nx-awesome-todos/ui';

<Todos todos={todos} />;
Enter fullscreen mode Exit fullscreen mode

PD: Because Nx is a monorepo (All node_modules are at the root level), we can use Next.js components inside our React components like Link, Image, etc.

Setup Storybook

nx g @nrwl/react:storybook-configuration ui
Enter fullscreen mode Exit fullscreen mode

Go to libs/ui/src/lib/ui.stories.tsx, add the todos to args

import { Todo } from '@nx-awesome-todos/shared-types';
import { Story, Meta } from '@storybook/react';
import { Todos, TodosProps } from './todos';

export default {
  component: Todos,
  title: 'Todos',
} as Meta;

const todos: Todo[] = [
  { id: 1, content: 'First todo', completed: true },
  { id: 2, content: 'Second todo', completed: false },
];

const Template: Story<TodosProps> = (args) => <Todos {...args} />;

export const Primary = Template.bind({});
Primary.args = {
  todos,
};
Enter fullscreen mode Exit fullscreen mode

Run storybook on the ui project

nx run ui:storybook
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
carlosdubon profile image
Carlos Dubón

The only problem I find with monorepos and their shared library is that it only works locally for me. Once I try to make it work on the cloud (Google Cloud Platform's App Engine) the builder throws an error saying the shared library doesn't exist. I think this problem occurs because one needs to specify a working directory. Does Nx solve this problem? Or should I switch my cloud provider?