Tech

The Ultimate JS Stack - Next.js, Apollo and Adonis GraphQL

by Ferruccio BalestreriAug 20 2019

Have you ever worked on a Node back-end? If you did, what was the biggest pain point for you?

In my experience, the worst part of working on a Node project is the fact that you have to pick a dozen of packages to get something working.

When starting out I always wanted to have an opinionated setup by a better developer than me to get started with something.

Here's the tech stack you'll learn how to set up in this post:

  • SQLite (or Postgres if you set it in the environment variables)
  • Adonis
  • GraphQL APIs
  • Apollo
  • Next.js (React Rendered on Server Side)
  • Styled Components

You can check out the full project source here:

https://github.com/CREATORSNEVERDIE/adonis-graphql-nextjs-starter

I like to use a simple SQLite database on most simple CRUD app use cases that will never have to scale to millions of users.

Using Mongo, would give you more flexibility, but is almost always a bad idea you regret later, given it's NoSQL.

Using Adonis, you get SQLite and Postgres support out of the box, with an ORM and migrations abstracted for you.

The goal with Adonis is to be the Rails or Django for Javascript.

This means getting you up and running as fast as possible with an opinionated, but extensible project structure.

Building large projects in fact you quickly grow out of the Adonis templating language and want to use something like React. But you don't want to have the problems related to SPAs, like difficult SEO and heavy pages so you opt for Server Side Rendering with Next.js.

Also as the work to set up some basic REST APIs is the same as setting up GraphQL APIs you also want to have those, and use Apollo on the front end.

This is for me the most elegant and most pleasant to work with Javascript stack.To have a deeper understanding of how all the pieces fit together I'll guide you through the setup I recently did for a client project.

Installing and Setting Up AdonisThe first thing you want to do is install Adonis running the following command:

npm i -g @adonisjs/cli

Once you got this up and running you can create a new project and run it with:

Replace "creatorsneverxx" with the name of your project

adonis new creatorsneverxx --blueprint=omarkhatibco/adonis-nextjs-starter && cd creatorsneverxx && npm run dev

As you can see from the code we aren't really using standard Adonis, but a blueprint that gives us Next.JS set up out of the box.

Now you can click on localhost:3333 and see:

That was quick! Right?The next step is to make our code more coherent installing a linter.

Create a .eslintrc file in your main project directory and paste this inside of it.

Also if you use VS Code it's a good idea to set up auto formatting on save. It takes a couple of minutes to do, but will save you hours in the future. --> https://youtu.be/YIvjKId9m2c

Creating the Models for our Database

Adonis comes with a predefined model to have users and authentication.To get our feet wet with the adonis models we'll first edit the existing User model to fit our needs and then we'll create a custom Model.To do this let's open the migration to create a user:

// database/migrations/[a-number-will-be-here]_user.js

'use strict'

const Schema = use('Schema')

class UserSchema extends Schema {
	up () {
        this.create('users', (table) => {
            table.increments()  

            // We don't need this field
            table.string('username', 80).notNullable().unique()

            table.string('email', 254).notNullable().unique()
            table.string('password', 60).notNullable()
            table.timestamps()
        })
	}
	
	down () {
		this.drop('users')
	}
}

module.exports = UserSchema

Let's edit the model to fit our use case.

'use strict'

const Schema = use('Schema')

class UserSchema extends Schema {
    up () {
        this.create('users', (table) => {table.increments()
              // Let's add a firstname and a lastname field
              table.string('firstname', 80).notNullable() 
              table.string('lastname', 80).notNullable()
              // Make sure you don't require them to be UNIQUE!

              table.string('email', 254).notNullable().unique()
              table.string('password', 60).notNullable()
              table.timestamps()
        })
    }

    down () {
        this.drop('users')}}

module.exports = UserSchema

Now that we changed our models, let's install SQLite and run the migrations on it.

npm i sqlite3 && adonis migration:run

Configuring Adonis GraphQL and Apollo

To Install the packages we'll use run the following command:Add to your adonis providers Adonis Graphql:

// in the "/start/app.js"// find the providers and add 'adonis-graphql/providers/GraphQLProvider'

const providers = [
    '@adonisjs/framework/providers/AppProvider',
    '@adonisjs/auth/providers/AuthProvider',
    '@adonisjs/bodyparser/providers/BodyParserProvider',
    '@adonisjs/cors/providers/CorsProvider',
    '@adonisjs/lucid/providers/LucidProvider',
    '@adonisjs/mail/providers/MailProvider',
    '@adonisjs/drive/providers/DriveProvider',
    'adonis-nextjs/providers/NextProvider',
    'adonis-graphql/providers/GraphQLProvider' // add this line
];

Add the GraphQL apis to the Routes:

// ....
const Next = use('Adonis/Addons/Next');
const GraphQLServer = use('Adonis/Addons/GraphQLServer'); 
const { graphiqlAdonis } = require('apollo-server-adonis');

// Add this line
// And Replace the REST APIs with the GraphQL endpoint

// Before:

Route.get('/api', ({ request }) => {
    return { greeting: 'Hello world in JSON' };});

// After:

Route.post('/api', ctx => GraphQLServer.handle(ctx));

// Setup the /graphiql route to show the GraphiQL UI
Route.get(
  '/graphiql',
  graphiqlAdonis({
    endpointURL: '/api', 
    // the POST endpoint that GraphiQL will make the actual requests to
  })
);

Now in the /config folder create a file named graphql.js

This is to tell Adonis where out Schema and Resolvers will be, so it knows where it can get them.For this tutorial we're going to put these files in the /app folder.

So here's how our code looks like:

// In /config/graphql.js (create this file)

'use strict'
const { join } = require('path')

module.exports = {
    schema: join(__dirname, '../app/Schema'), // Our Schema will be
    resolvers: join(__dirname, '../app/Resolvers'), // Our Resolvers
    options: {}
}

Now if you try running the app it won't work..

That's because we told Adonis "Run a GraphQL server and take the Schema and Resolvers from these two files", but didn't actually create the two files.Let's head over to the app/ folder and let's create a Schema file at app/Schema/schema.graphql


// In app/Schema/schema.graphql (create this file)

// All our Queries should go here
type Query {
    currentUser: User
}

// All our Mutations should go here
type Mutation {
    signup(email: String!, password: String!, firstname: String!, lastname: String!): AuthPayload
    login(email: String!, password: String!): AuthPayload
    logout: String
}

// All the Models we use go here
type User {
    id: Int!
    email: String!
    firstname: String!
    lastname: String!
}

// Then, if we want, we can define the Payloads, which are the objects we return from a Mutation or Query
type AuthPayload {
  token: String!
  user: User!
}

This code might look a little overwhelming, but here is what it does. We have a model (type) called "User" that has an id, email, a first name and a last name. All of these have to be Strings, except for the id, that is an Integer.

Then we have a Query, called currentUser, that returns an object that is a User.

Then we have some Mutations, called signup, login and logout.

Obviously the signup form requires more fields than the login form as we're creating an account, but the logic is the same.

Both login and signup, return an AuthPayload object, which contains a "token" field. This allows us to recognise our users.

Now that we defined our Schema, we have to write some Resolvers.

Resolvers are basically functions that "Resolve" the queries we have, loading the data we need from our Database and formatting it properly.

To get this done, let's create a Resolvers folder inside the app folder (so it's right next to the Schema).

// In app/Resolvers/Auth.js (create this file)

// These are the resolvers for our Authentication, so I put them all in the same file

const User = use('App/Models/User');

module.exports = {
  Query: {
    async currentUser(parent, args, ctx) {
      const user = await ctx.auth.getUser();
      return user.toJSON();
    },
  },

  Mutation: {
    async login(parent, { email, password }, ctx) {
      try {
        const { token } = await ctx.auth.attempt(email, password);
        const user = await User.findBy('email', email);
        ctx.response.plainCookie('token', token);
        return { user, token };
      } catch (err) {
        throw err;
      }
    },

    async signup(parent, { email, password, firstname, lastname }, ctx) {
      try {
        const user = await User.create({ email, password, firstname, lastname });
        const { token } = await ctx.auth.attempt(email, password);
        ctx.response.plainCookie('token', token);
        return { user, token };
      } catch (err) {
        throw err;
      }
    },

    async logout(parent, args, ctx) {
      try {
        ctx.response.clearCookie('token');
        return 'Successfully logged out';
      } catch (err) {
        throw err;
      }
    },
  },
};

We're now ready to go!

Open localhost:3333/graphiql and you'll be able to play around with your newly created GraphQL apis.

Test driving our APIs with GraphiQL

Opening up GraphiQL, we can now try and Signup a user, and then log in.

To do so let's create the Mutation to signup a user (that we'll use in the front end):



```

Try running this and...

```json

{
  "data": {
    "signup": {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjIsImlhdCI6MTU0OTk5MTMyMn0.YoNpGLVBv8Elfyh9CpDa-hWTjHDfRa4mPMNQuc8GVQ0"
    }
  }
}

```

..And it's working!

## Setting Up Apollo and Styled Components

Now that we nailed the backend part of our App, let's move to the frontend.

Using Next.js we have the advantage of not having to manually write a "routes" file.

But as we want to load some things like the CSS contained in our Styled Components on the Server, and use Apollo for managing GraphQL calls we'll have to write an _app.js file and a _document.js file in the pages folder.

**Setting up Apollo**

To move forward we'll first need to install a library to handle cookies and one to fetch our API endpoints with Apollo:

```javascript
npm i isomorphic-unfetch nookies apollo-upload-client styled-components
```

Now that we've done this let's create the Apollo configuration:

```javascript
// In next/lib/initApollo.js (create this file and folder)

import { ApolloClient, InMemoryCache } from 'apollo-boost';
import { createUploadLink as CreateUploadLink } from 'apollo-upload-client';
import fetch from 'isomorphic-unfetch';

let apolloClient = null;
const endpoint = 'http://localhost:3333/api';
// Polyfill fetch() on the server (used by apollo-client)
if (!process.browser) {
  global.fetch = fetch;
}

function create(initialState, { getToken }) {
  // Check out https://github.com/zeit/next.js/pull/4611 if you want to use the AWSAppSyncClient
  const token = getToken();
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser, // Disables forceFetch on the server (so queries are only run once)
    link: new CreateUploadLink({
      uri: process.env.NODE_ENV === 'development' ? endpoint : endpoint, // Server URL (must be absolute)
      credentials: 'include',
      headers: { authorization: `Bearer ${token}` },
    }),
    cache: new InMemoryCache().restore(initialState || {}),
  });
}

export default function initApollo(initialState, options) {
  // Make sure to create a new client for every server-side request so that data
  // isn't shared between connections (which would be bad)

  if (!process.browser) {
    return create(initialState, options);
  }

  // Reuse client on the client-side
  if (!apolloClient) {
    apolloClient = create(initialState, options);
  }

  return apolloClient;
}

```

This will be used to initialise Apollo for each user that uses our site.

We'll create also a withApollo component, that will call the function we just created.

```jsx

// In next/lib/withApollo.js

import React from 'react';
import Head from 'next/head';
import { getDataFromTree } from 'react-apollo';
import nookies from 'nookies';
import initApollo from './initApollo';

export default App =>
  class Apollo extends React.Component {
    static async getInitialProps(ctx) {
      const {
        Component,
        router,
        ctx: { req, res },
      } = ctx;

      const token = req
        ? nookies.get(ctx.ctx).token
        : document.cookie.split('=')[1];

      const apollo = initApollo(
        {},
        {
          getToken: () => token,
        }
      );

      ctx.ctx.apolloClient = apollo;

      let appProps = {};
      if (App.getInitialProps) {
        appProps = await App.getInitialProps(ctx);
      }

      if (res && res.finished) {
        // When redirecting, the response is finished.
        // No point in continuing to render
        return {};
      }

      if (!process.browser) {
        // Run all graphql queries in the component tree
        // and extract the resulting data
        try {
          // Run all GraphQL queries
          await getDataFromTree(
            <App
              {...appProps}
              Component={Component}
              router={router}
              apolloClient={apollo}
            />
          );
        } catch (error) {
          // Prevent Apollo Client GraphQL errors from crashing SSR.
          // Handle them in components via the data.error prop:
          // https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
          console.error('Error while running `getDataFromTree`', error);
        }

        // getDataFromTree does not call componentWillUnmount
        // head side effect therefore need to be cleared manually
        Head.rewind();
      }

      // Extract query data from the Apollo's store
      const apolloState = apollo.cache.extract();

      return {
        ...appProps,
        apolloState,
        token,
      };
    }

    constructor(props) {
      super(props);
      // `getDataFromTree` renders the component first, the client is passed off as a property.
      // After that rendering is done using Next's normal rendering pipeline
      this.apolloClient = initApollo(props.apolloState, {
        getToken: () => props.token,
      });
    }

    render() {
      return <App {...this.props} apolloClient={this.apolloClient} />;
    }
  };

```

Now that we've created the Apollo configuration we want to load it into all of our pages!

To do this we create a _app.js file in the /pages folder of Next.

```jsx
// In next/pages/_app.js

import React from 'react';
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';

import withApollo from '../lib/withApollo';


export default withApollo(
  class MyApp extends App {
    static async getInitialProps({ Component, ctx }) {
      let pageProps = {};
      if (Component.getInitialProps) {
        pageProps = await Component.getInitialProps(ctx);
      }

      return { pageProps };
    }

    render() {
      const { Component, pageProps, apolloClient } = this.props;

      return (
        <Container>
          <ApolloProvider client={apolloClient}>
              <Component {...pageProps} />
          </ApolloProvider>
        </Container>
      );
    }
  }
);

```

The last step now is to get styled-components working with Next.js (so they're rendered on the server).

To get this last task done we have to create a _document.js file, right next to the _app.js file.

```jsx

// In next/pages/_document.js (create this file!)

import Document, { Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

export default class MyDocument extends Document {
  static async getInitialProps({ renderPage }) {
    const sheet = await new ServerStyleSheet();

    const page = await renderPage(App => props =>
      sheet.collectStyles(<App {...props} />)
    );

    const styleTags = sheet.getStyleElement();

    return { ...page, styleTags };
  }

  render() {
    return (
      <html lang="en">
        <Head>
          {this.props.styleTags}
          <meta name="viewport" content="width=device-width, initial-scale=1" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    );
  }
}


```


By adding this component, we are rendering the stylesheets and adding them to the page source, so the styled-components become part of one css bundle.

The app is ready now, but let's try it out!

The app is fully setup to work.

Let's add a couple of pages that do the authentication, style them with Styled Components and implement the GraphQL Mutations and Queries.

Creating Users

Let's move forward and create a page to Authenticate our users.

Generally I like keeping the pages folder with just the routes and a basic functional component that calls other components.

So let's create a page to handle Signups:

/ In next/pages/signup.js

import Signup from '../components/Signup';

export default function SignupPage() {
  return <Signup />;
}


```

And let's move to the components folder to create the actual signup view.

```javascript

// In next/components/Signup/index.js
import React, { Component } from 'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';

import Error from '../ErrorMessage';
import { AuthHeader, AuthForm } from '../style';

const SIGNUP_MUTATION = gql`
  mutation SIGNUP_MUTATION(
    $email: String!
    $password: String!
    $firstname: String!
    $lastname: String!
  ) {
    signup(
      email: $email
      password: $password
      firstname: $firstname
      lastname: $lastname
    ) {
      token
      user {
        id
        email
      }
    }
  }
`;

export default class Signup extends Component {
  state = {
    email: '',
    password: '',
    firstname: '',
    lastname: '',
    error: '',
    loading: '',
  };

  handleChange = e => {
    const { name, value } = e.target;
    const val = value;
    this.setState({ [name]: val });
  };

  render() {
    const { error, loading, firstname, lastname, email, password } = this.state;
    return (
      <div>
        <AuthHeader>Sign Up</AuthHeader>
        <Mutation mutation={SIGNUP_MUTATION} variables={this.state}>
          {(signupUser, { loading }) => (
            <AuthForm
              onSubmit={async e => {
                e.preventDefault();
                try {
                  await signupUser();
                  window.location.href = window.location.origin;
                } catch (err) {
                  this.setState({ error: err });
                }
              }}
              method="POST"
            >
              <Error error={error} />
              <input
                type="text"
                placeholder="first name"
                name="firstname"
                id="firstname"
                value={firstname}
                onChange={this.handleChange}
              />
              <input
                type="text"
                placeholder="last name"
                name="lastname"
                id="lastname"
                value={lastname}
                onChange={this.handleChange}
              />
              <input
                type="text"
                placeholder="email"
                name="email"
                id="email"
                value={email}
                onChange={this.handleChange}
              />
              <input
                type="password"
                placeholder="password"
                name="password"
                id="password"
                value={password}
                onChange={this.handleChange}
              />
              <input type="submit" value="login" />
            </AuthForm>
          )}
        </Mutation>
      </div>
    );
  }
}


```


And here are some styles I made for the page:

```javascript

// In next/components/Signup/style.js

import styled from 'styled-components';

export const AuthHeader = styled.h1`
  text-align: center;
  font-weight: 800;
  font-size: 2.5em;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
`;

export const AuthForm = styled.form`
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
    Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
  padding: 20px;
  font-size: 1.2rem;
  line-height: 1.5;
  font-weight: 600;
  display: flex;
  flex-direction: column;
  align-items: center;

  label {
    display: block;
    margin-top: 1rem;
    margin-bottom: 0.5rem;
    font-size: 11px;
    line-height: 16px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: rgba(55, 53, 47, 0.6);
    font-weight: 500;
  }

  input {
    margin-top: 3px;
    display: block;
    width: 400px;
    max-width: 100%;
    padding: 1rem;
    font-size: 1rem;
    margin-top: 1rem;
    background-color: #efefef;
    border-radius: 4px;
    border: 1px solid #efefef;
    &:focus {
      outline: 0;
      border: 1px solid ${props => props.theme.colorDarkLight};
    }
  }
  button,
  input[type='submit'] {
    margin-top: 1rem;
    width: 435px;
    max-width: 100%;
    background: #fdf3f1;
    border: 0;
    border-radius: 2px;
    font-size: 1rem;
    font-weight: 400;
    padding: 0.5rem 1.2rem;
    padding: 1rem;
    border: 1px solid #f7cbcb;
    color: #ea5354;
    cursor: pointer;
  }
  fieldset {
    border: 0;
    padding: 0;

    &[disabled] {
      opacity: 0.5;
    }
  }
`;

```

Now you can visit the /signup route and test it out.

At the moment when logging in we're saving the JWT token in a cookie, but we aren't doing anything with it.

So let's create an Apollo component that checks for Authentication and "protects" routes or components:


```javascript

// In next/lib/WithAuth/index.js

import React from 'react';
import { Mutation } from 'react-apollo';

import isLoggedIn from './isLoggedIn';
import { LOGIN_MUTATION, LOGOUT_MUTATION } from './mutations';
import redirect from './redirect';

export default function withAuth(WrappedComponent, options = {}) {
  return class WithAuth extends React.Component {
    static async getInitialProps(context) {
      const { apolloClient } = context;
      const { protectedRoute = true } = options;
      let user;

      try {
        const {
          data: { currentUser },
        } = await isLoggedIn(apolloClient);
        user = currentUser;
      } catch (e) {
        if (protectedRoute) {
          redirect(context, '/login');
        }
      }

      return { user };
    }

    login = async (loginMutation, email, password) => {
      try {
        await loginMutation({ variables: { email, password } });
      } catch (err) {
        throw err;
      }
    };

    logout = async logoutMutation => {
      try {
        await logoutMutation();
      } catch (e) {
        throw e;
      }
    };

    render() {
      const { user } = this.props;
      return (
        <Mutation mutation={LOGIN_MUTATION}>
          {login => (
            <Mutation mutation={LOGOUT_MUTATION}>
              {logout => (
                <WrappedComponent
                  user={user}
                  logout={() => this.logout(logout)}
                  login={(email, password) =>
                    this.login(login, email, password)
                  }
                />
              )}
            </Mutation>
          )}
        </Mutation>
      );
    }
  };
}


```

```javascript

// In next/lib/WithAuth/isLoggedIn.js

import gql from 'graphql-tag';

export const CURRENT_USER = gql`
  query currentUser {
    currentUser {
      email
      id
    }
  }
`;

const isLoggedIn = async apolloClient =>
  apolloClient.query({ query: CURRENT_USER });
export default isLoggedIn;


```

```javascript

// In next/lib/WithAuth/mutations.js

import gql from 'graphql-tag';

export const LOGIN_MUTATION = gql`
  mutation LOGIN_MUTATION($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      user {
        email
        id
      }
    }
  }
`;

export const LOGOUT_MUTATION = gql`
  mutation LOGOUT_MUTATION {
    logout
  }
`;



```


```javascript

// In next/lib/WithAuth/redirect.js

import Router from 'next/router';

export default (context, target) => {
  if (context.res) {
    context.res.writeHead(303, { Location: target });
    context.res.end();
  } else {
    Router.replace(target);
  }
};


```

**Setting up private routes**

So now we can create a private page, with some secret content and only show it to authenticated users:

```javascript

// In next/pages/private.js

import React from 'react';
import withAuth from '../lib/withAuth';

const Private = () => <div>Top Secret Page</div>;

export default withAuth(Private);

```


If you try visiting this page while logged out, you'll get redirected to `/login`.

So let's implement the login page too:

```javascript

// In next/pages/login.js

import Login from '../components/Login';

export default function LoginPage() {
  return <Login />;
}

```

```javascript

// In next/components/Login/index.js

import React, { Component } from 'react';
import Router from 'next/router';
import { AuthHeader, AuthForm } from '../Signup/style';
import Error from '../ErrorMessage';
import withAuth from '../../lib/withAuth';

class LoginPage extends Component {
  state = {
    email: '',
    password: '',
    error: '',
    loading: '',
  };

  handleChange = e => {
    const { name, value } = e.target;
    const val = value;
    this.setState({ [name]: val });
  };

  login = async e => {
    e.preventDefault();
    const { login } = this.props;
    const { email, password } = this.state;
    try {
      await login(email, password);
      Router.push('/');
    } catch (error) {
      this.setState({ error });
    }
  };

  render() {
    const { error, loading, email, password } = this.state;

    return (
      <>
        <AuthHeader>Login</AuthHeader>
        <AuthForm onSubmit={this.login}>
          <Error error={error} />
          <fieldset disabled={loading} aria-busy={loading}>
            <label htmlFor="email">
              Email
              <input
                type="email"
                name="email"
                id="email"
                value={email}
                onChange={this.handleChange}
                autoComplete="email"
              />
            </label>
          </fieldset>
          <fieldset disabled={loading} aria-busy={loading}>
            <label htmlFor="password">
              Password
              <input
                type="password"
                name="password"
                id="password"
                value={password}
                onChange={this.handleChange}
                autoComplete="password"
              />
            </label>
          </fieldset>
          <button type="submit">Login</button>
        </AuthForm>
      </>
    );
  }
}

export default withAuth(LoginPage);

```

And that's it for this tutorial!

Hope you had fun learning how to set up this stack. Let me know if you need any help, or if you're running into any problems, by DMing me on Twitter

Recommended Posts

signature

© 2019 Creators Never Die. All Right Reserved. Made with Magic World Wide

About Creators Never Die

Creators Never Die. Is a publication that focuses on content for creators.