aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.42k stars 2.12k forks source link

Server-Side Rendering (SSR) with Next.js #5435

Closed ericclemmons closed 3 years ago

ericclemmons commented 4 years ago

Given Next.js’ popularity, customer impact, and being a hybrid framework supporting both SSR & SSG, solving for Next.js first unblocks other frameworks (e.g. Gatsby, Nuxt).

There is additional work to be done for CLI & Console support (particularly around deployments), but this Epic focuses on JS support.

This feature is live! See the Getting Started tutorial and SSR docs for the latest!

Public Preview

We'd love your help testing out Amplify's API, Auth, and DataStore categories in your Next.js app!

  1. First, reinstall Amplify using the @preview tag on NPM:

    yarn add aws-amplify@preview
    # or
    npm install aws-amplify@preview --save
  2. If you're only accessing public data (e.g. API.graphql(listBlogs)), you're done! 😌

  3. For secure access the current user on the server, you'll need to scope Amplify to the current request (and only that request) using our new withSSRContext helper:

    diff --git a/my-next-app/pages/index.tsx b/my-next-app/pages/index.tsx
    index 22b0c471..d7ce454b 100644
    --- a/my-next-app/pages/index.tsx
    +++ b/my-next-app/pages/index.tsx
    @@ -1,6 +1,6 @@
     import { CognitoUser } from '@aws-amplify/auth'
     import { GRAPHQL_AUTH_MODE } from '@aws-amplify/api-graphql'
    -import { Amplify, API, Auth } from 'aws-amplify'
    +import { Amplify, withSSRContext } from 'aws-amplify'
     import { Authenticator } from 'aws-amplify-react'
     import { GetServerSideProps } from 'next'
     import Head from 'next/head'
    @@ -13,8 +13,6 @@ import { CreateTodoMutationVariables } from '../src/API'
    
     Amplify.configure(awsconfig)
    
     // 👇 Your frontend code is now "SSR-aware", giving the backend access to the current session
    +const { Auth, API } = withSSRContext()
    +
     export default function Home(props) {
    @@ -392,8 +390,6 @@ export default function Home(props: Props) {
     }
    
     export const getServerSideProps: GetServerSideProps = async (context) => {
       // 👇 Auth & API are now scoped to the _current_ user's session
    +  const { Auth, API } = withSSRContext(context)
    +

    For more information on withSSRContext, see https://github.com/aws-amplify/amplify-js/pull/6146.

  4. Last step! Let us know your experience in the comments below 👇

Milestones


Related milestone: https://github.com/aws-amplify/amplify-js/milestone/26 Related issues: #5101, #4178, #3741, #1613, #3278, #5121, #5097, #4972, #2230, #4990, #4851, #5293, #5435, #5322, #3854, #3053, #3348, #5138, #4311, #4305, #4207, #3037, #992

janhesters commented 4 years ago

Tried the @preview versions of @aws-amplify/auth and @aws-amplify/core out with Next.js and I get:

error - ./node_modules/@aws-sdk/client-cognito-identity/dist/es/runtimeConfig.browser.js
Should not import the named export 'name' (imported as 'name') from default-exporting module (only default export is available soon)

with both packages. When I got into the node_modules and into runtimeConfig.browser.js and instead of importing the name and version from package.json I hardcode them, the code works. @ericclemmons you might need to change the way you bundle these packages 🀔 .

ericclemmons commented 4 years ago

@janhesters Can you share more of your code & your Next.js version? The demo app I'm running integration tests against uses aws-amplify@preview as a dependency to access the withSSRContext helper and haven't experienced that.

jfryerocv commented 4 years ago

@ericclemmons will this work if someone creates a completely static app with next export?

ianmartorell commented 4 years ago

@ericclemmons will this work if someone creates a completely static app with next export?

If you create a completely static app you don't need server side rendering anymore.

ericclemmons commented 4 years ago

@jfryerocv Yes, @ianmartorell is correct. The majority of Amplify worked with Next.js' SSG since v3 was released earlier this year.

There is some work here to support DataStore within that environment, but otherwise the withSSRContext isn't needed at all for SSG.

janhesters commented 4 years ago

@ericclemmons I tried both, running @preview and the stable one and both display that behaviour. Not using withSSRContext because we use sagas. Instead we use Auth but all the code is only called on the client-side.

I have an issue here, which is for another problem, but has details about my Next.js setup.

ericclemmons commented 4 years ago

@janhesters I'll check out https://github.com/aws-amplify/amplify-js/issues/6555, since that looks like the crux of the problem (and can be tested in CRA to reduce this being SSR-specific).

janhesters commented 4 years ago

@ericclemmons I think #6555 might have a different reason though. I'll try and create a minimal setup for https://github.com/aws-amplify/amplify-js/issues/5435#issuecomment-672741212 soon 👍

dberhane commented 4 years ago

It would be great to know if you have plans to support Next.js incremental static site generation and when?

ericclemmons commented 4 years ago

@dberhane This PR supports both SSG & SSR! If you're using the Amplify API category for your data, I believe aws-amplify@master will already work with SSG. For a static site (SSG), Auth will happen on the client & should work as usual.

oahmaro commented 4 years ago

is there a specific deadline for this PR? i am planing to use amplify with Next JS for my upcoming project and would like to know if this would be ready within 1 or 2 months? if so this would help me a lot as i can start implementing my business logic and UI, and add amplify once this PR is merged. Really appreciate the work you guys are doing here. Thanks.

apoorvmote commented 4 years ago

I am having difficult time serving api at path mydomain.com/api/* because I am serving static site at path mydomain.com/.

Let me prefix by saying I am using CDK and cloudfront/s3 for static site. And I am not able to forward request from cloudfront to api gateway. Cloudfront catches api path requests and returns 404 not found. I have made issue with CDK and they are working on it.

But tomorrow if I give up CDK/Cloudformation and go back to amplify with Nexjs. Is it possible to create custom domain api like mydomain.com/api/* and have static site on mydomain.com/ without any conflicts and on HTTPS connection?

Sonicrida commented 4 years ago

I just wanted to say thanks for the hard work being done here. Just got Next working with Amplify Auth!

Mdev303 commented 4 years ago

@ericclemmons Hello , is there any plan to do something similar for nuxtjs ?

loukmane-issa commented 4 years ago

First of all, thanks a lot @ericclemmons for all the work being done here. I have been able to get the authentication working on a page using SSR and the documentation you provided here. I have a question though, I was planning to use React Context to store if the user has been authenticated or not. This would allow me to conditionally choose layout and also make other logic for pages based on the authentication status of the user.

However, when I am trying to use the following code in the getInitialProps in my _app.tsx:

    const { Auth } = withSSRContext(context);
    let isUserAuthenticated = false;
    try {
      const user = await Auth.currentAuthenticatedUser();
      if (user) {
        isUserAuthenticated = true;
      }
    } catch (e) {
      console.error(e);
    }

The result always bring a non authenticated user. However, if I use the getInitialProps of the actual page, this will result in isUserAuthenticated being true. Do you know why this could happen?

Thanks.

Nickman87 commented 4 years ago

I'm using this preview and got Authentication up and running. However I cannot seem to get API working trough the withSSRContext.

When I do the following: const { Auth, API } = withSSRContext(); API is null, even though the API works fine using import { API } from "aws-amplify";.

I'm using "aws-amplify": "^3.0.18-preview.46",

Is there something I can do to debug this?

Another question: when will the preview get a version update? The current version is already at 24 :)

Looking forward to get this fully up and running!

ericclemmons commented 4 years ago

@loukmane-issa Can you help me understand exactly what's happening here?

The result always bring a non authenticated user. However, if I use the getInitialProps of the actual page, this will result in isUserAuthenticated being true. Do you know why this could happen?

It sounds like, on the server, the user isn't authenticated. But running that logic on the client works (as expected)? I did find a bug recently with this, but haven't updated @preview. Let me know if if my understanding is correct!


@Nickman87 Can you share a snippet of your code? I was aware of this potential problem, but haven't been able to replicate. Here's my API usage:

// pages/api/create-todo.tsx
import { Amplify, withSSRContext } from 'aws-amplify'
import { GraphQLResult } from '@aws-amplify/api-graphql'
import { NextApiRequest, NextApiResponse } from 'next'

import { CreateTodoMutationVariables, CreateTodoMutation } from '../../src/API'
import awsconfig from '../../src/aws-exports'
import { createTodo } from '../../src/graphql/mutations'

Amplify.configure(awsconfig)

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { API } = withSSRContext({ req })

  try {
    const query = createTodo
    const variables: CreateTodoMutationVariables = {
      input: {
        title: `New Todo ${new Date().toLocaleString()}`,
      },
    }

    const { data } = (await API.graphql({
      query,
      variables,
    })) as GraphQLResult<CreateTodoMutation>

    return res.status(200).json({ data })
  } catch (error) {
    console.error(error)
    return res.status(500).json({ error })
  }
}

@Nickman87 If you're open to discussing this live, you can DM on me Twitter (@ericclemmons) or find me in our Discord: https://discord.gg/Px5YVfQ

MontoyaAndres commented 4 years ago

@ericclemmons is a good practice to call the method Amplify.configure just once in the page _app.js or is necessary to call it on each page?

loukmane-issa commented 4 years ago

@ericclemmons about:

It sounds like, on the server, the user isn't authenticated. But running that logic on the client works (as expected)? I did find a bug recently with this, but haven't updated @preview. Let me know if if my understanding is correct!

I don't believe this is what's happening. I am not a Next.js expert, but from what I understand, The getInitialProps will either run on the server or on the client depending if it's the first load. In both cases, the authentication is working properly if I place the authentication code within the get initial props of the page itself (e.g. pages/dashboard.tsx). However, if I use the getInitialProps of _app.ts, I get no user from the currentAuthenticatedUser() method. The main reason for doing it in the _app.ts is to have the user and the authentication status available within a Context and easily access this throughout my application.

As I just started development, I have refactored my app in order to not use authentication status within a context and have the check in each page getInitialProps but I would like to go back using Context if it is possible.

Danm72 commented 4 years ago

I'm getting no current user when using the preview

aquiseb commented 4 years ago

Thanks so much @ericclemmons it works well in Next.js. I'm not completely done with the refactor, but if anyone needs a Next.js example, here is mine:

Install AWS Amplify @preview

yarn add aws-amplify@preview

# Optional
yarn add hoist-non-react-statics

Create a HOC that will be used in _app.js

// libs/withAmplify.js
import * as React from "react";
import { Amplify, withSSRContext } from "aws-amplify";
import hoistNonReactStatics from "hoist-non-react-statics";

const AmplifyContext = React.createContext();
export const useAmplify = () => React.useContext(AmplifyContext);

export const withAmplify = (ComposedComponent) => {
    const WithAmplify = (props) => {
        const { amplifyConfig, ...rest } = props;
        const { Auth } = withSSRContext();

        React.useMemo(() => {
            Amplify.configure(amplifyConfig);
            Auth.configure(amplifyConfig);
        }, []);

        return (
            <AmplifyContext.Provider value={{ amplifyConfig }}>
                <ComposedComponent {...rest} />
            </AmplifyContext.Provider>
        );
    };

    WithAmplify.getInitialProps = async (appContext) => {
        const { Auth } = withSSRContext(appContext.ctx);
        const amplifyConfig = {
            Auth: {
                region: process.env.AWS_AMPLIFY_REGION,
                userPoolId: process.env.AWS_AMPLIFY_USER_POOL_ID,
                userPoolWebClientId: process.env.AWS_AMPLIFY_USER_POOL_WEB_CLIENT_ID,
                mandatorySignIn: true,
                authenticationFlowType: process.env.AWS_AMPLIFY_AUTHENTICATION_FLOW_TYPE,
                oauth: {
                    domain: process.env.AWS_AMPLIFY_OAUTH_DOMAIN,
                    scope: process.env.AWS_AMPLIFY_OAUTH_SCOPE,
                    redirectSignIn: process.env.AWS_AMPLIFY_OAUTH_REDIRECT_SIGN_IN,
                    redirectSignOut: process.env.AWS_AMPLIFY_OAUTH_REDIRECT_SIGN_OUT,
                    responseType: process.env.AWS_AMPLIFY_OAUTH_RESPONSE_TYPE,
                },
            },
        };

        if (typeof window === 'undefined') {
            Amplify.configure(amplifyConfig);
            Auth.configure(amplifyConfig);
        }

        return {
            amplifyConfig,
        };
    };

    return hoistNonReactStatics(WithAmplify, ComposedComponent);
};

Use withAmplify in _app.js

Wrap your App with withAmplify HOC previously created.

// pages/_app.js
import * as React from "react";
import { withAmplify } from "../libs/withAmplify";

const MyApp = ({ Component, pageProps }) => {
    return <Component {...pageProps} />;
};

export default withAmplify(MyApp);

Create a login page

// pages/login.js
import * as React from "react";
import { withSSRContext } from "aws-amplify";
import Router from "next/router";

const Login = (props) => {
    const [state, _setState] = React.useState({
        username: "",
        password: "",
    });
    const setState = (newState) => _setState({ ...state, ...newState });

    // 👇 Your frontend code is now "SSR-aware", giving the backend access to the current session
    const { Auth } = withSSRContext();

    React.useEffect(() => {
        (async () => {
            let user, currentSession;
            try {
                user = await Auth.currentAuthenticatedUser();
            } catch (e) {
                console.error("currentAuthenticatedUser error:", e);
            }

            try {
                currentSession = await Auth.currentSession();
            } catch (e) {
                console.error("currentSession error:", e);
            }

            // if (currentSession) Router.push("/");
            console.log("login.user --", user);
            console.log("login.currentSession --", currentSession);
        })();
    }, []);

    const handleChange = (e) => {
        setState({ [e.target.id]: e.target.value });
    };

    const federatedSignIn = () => {
        Auth.federatedSignIn();
    };

    const signIn = async (e) => {
        e.preventDefault();
        try {
            await Auth.signIn(state.username, state.password);
            Router.push("/");
        } catch (err) {
            if (err.code === "InvalidParameterException" || err.code === "NotAuthorizedException") alert("Incorrect username or password.");
            else alert(err.message);
        }
    };

    return (
        <>
            <h1>Login page</h1>
            <form onSubmit={signIn}>
                <input id="username" type="text" value={state.username} onChange={handleChange} />
                <input id="password" type="password" value={state.password} onChange={handleChange} />
                <button type="submit" onClick={signIn}>
                    Sign in
                </button>
            </form>
            <br />
            <button onClick={federatedSignIn}>Hosted UI</button>
        </>
    );
};

export default Login;
aquiseb commented 4 years ago

While digging deeper in my implementation, I was hoping to override the path of the cookies options via cookieStorage, however, this has no effect on the CognitoIdentityServiceProvider cookies. With the above example, the cookies set on signIn() have their path automatically set to /login. As a consequence, other pages can't have the logged in user on SSR.

Adding path: "/" here https://github.com/aws-amplify/amplify-js/commit/a579c9b66eec99e771da92e35ed179bb4f2f984d#diff-ac47a9e216a032c74ba8bf10c36119b3R106 seems to solve this.

Is there any reason not to enable the path of the cookies to be overriden?

Thanks in advance for your insight

ericclemmons commented 4 years ago

@astenmies Good catch! I've locally updated UniversalStorage to add path: "/".

There's not a compelling reason yet to allow that customization, as I'd like to get more real-world usage examples to figure out what signature UniversalStorage should have. (The fact that it uses cookies behind the scenes is an implementation detail with it's own flaws)

ericclemmons commented 4 years ago

@dberhane Are you referring to the revalidate property in https://github.com/vercel/next.js/discussions/11552?

This effort is ensuring Amplify works within Next.js v9.3's SSG/SSR methods, so if you're deploying to Vercel (which takes advantage of revalidate), this should work as expected 👍

ericclemmons commented 4 years ago

@MontoyaAndres I'd recommend putting Amplify.configure in _App.js. For /api routes, I don't think that's used, so you'll need to duplicate Amplify.configure in those files. (Or put it in a utility folder and import from there)

MontoyaAndres commented 4 years ago

Thank you @ericclemmons !

Danm72 commented 4 years ago

@ericclemmons do you have any sample project you could share that shows everything working together?

I'm having a good bit of trouble and trying to piece together the configuration is a little tricky!

Really appreciate your work here!

WojciechMatuszewski commented 4 years ago

Hi guys, I've made a short video showcasing how to use Auth within getServerSideProps

https://www.youtube.com/watch?v=Fc5kgSq1lpw

Also here is the repo of a sample project I've made - authenticating and fetching data within getServerSideProps.

https://github.com/WojciechMatuszewski/next-appsync-cdk

Everything is done using withSSRContext. Thank you @ericclemmons for your awesome work!

Danm72 commented 4 years ago

@WojciechMatuszewski Hey! Code looks great. If you use user pool auth does it still work for you? Or what auth are you using for your schema/models?

For example: amplify update api

update auth user pool

Does it still work? I'm still getting 'no current user'

WojciechMatuszewski commented 4 years ago

@Danm72 I'm using user pools since I'm using Cognito (see config.json within frontend directory). I cannot speak for amplify-cli as I do not use that tool.

dabit3 commented 4 years ago

@WojciechMatuszewski nice video, thanks for sharing!!

Edit: Just watched the whole video, amazing explanation! We will definitely be sharing this once the release is officially announced if that is ok with you.

gbouteiller commented 4 years ago

First of all, great job for the feature!! However, if you only use it for Auth, it looks like it adds a lot to the bundle size. As withSSRContext is imported from aws-amplify, maybe it's because of a tree shaking problem?

WojciechMatuszewski commented 4 years ago

@WojciechMatuszewski nice video, thanks for sharing!!

Edit: Just watched the whole video, amazing explanation! We will definitely be sharing this once the release is officially announced if that is ok with you.

Sure, no problem. Thanks!

wmcons commented 4 years ago

Thanks @ericclemmons,

Just noticed, API.post works fine from client but server side API.post its throwing error with 'The security token included in the request is invalid.' using Auth,API from withssrcontext. Rest API is iam authenticated. But if i change the service cognito authorizer it does work. Any example with withssrcontext server API.post works with iam authenticated rest api.

Danm72 commented 4 years ago

The details in the above PR helped me a lot to get things running

ericclemmons commented 3 years ago

👋 Hi everyone!

Today, I'm proud to say that we've officially released SSR support for Amplify JS!

What's changed?

There have been a few changes since aws-amplify@preview was published (see the resources above for more):

  1. Configure your application with Amplify.configure({ ...awsExports, ssr: true }) so that it's "SSR-aware".

  2. Client-side code should stay the same as before:

    import { Amplify, API } from "aws-amplify"`;
    
    ...
    const { data } = await API.graphql({ query, variables })

    Using withSSRContext on the client will hurt client-side bundle sizes!

  3. Server-side code should use the new withSSRContext utility:

    import { Amplify, withSSRContext } from "aws-amplify";
    ...
    export async function getServerSideProps({ req }) {
        const SSR = withSSRContext({ req });
        const { data } = await SSR.API.graphql({ query, variables });
        ...

    withSSRContext creates a new copy of Amplify that's scoped to a single request (including credentials!).

Check out the Getting Started tutorial for a working example of this usage!

What about deployment?

If you're site is fully static (no getServerSideProps), you can continue using Amplify Console!

However, if you're using getServerSideProps or fallback: true, there are other options:

- Until [Amplify Console supports SSR](https://github.com/aws-amplify/amplify-console/issues/412) (be sure to upvote & comment!), we've used & tested [deploying with serverless framework](https://docs.amplify.aws/start/getting-started/hosting/q/integration/next).
- Of course, several customers are [deploying with Vercel](https://nextjs.org/docs/deployment) and that should continue working, too.

What's next?

With today's announcement, API (both GraphQL & REST), Auth, and DataStore are supported.

These features should also work with other frameworks (such as Nuxt.js & Gatsby), but we trust and rely on your input to make it even better.

The same goes for other features that didn't make it to release today (e.g. Analytics, Storage, etc.). Please share SSR-specific use-cases for those categories in a new issue on how Amplify can be better experience, and we'll be happy to help!

With that, I want to personally thank everyone on this issue for your feedback and effort. I'm thrilled that I could play a part in bringing Next.js support to Amplify JS, and want to continue working with y'all to make it even better!

Thanks!

MontoyaAndres commented 3 years ago

Thank you @ericclemmons and all the team behind this awesome work! 🊟

omar-dulaimi commented 3 years ago

Hello, where can I find docs that explain SSR setup using Nextjs and Amplify?

Danm72 commented 3 years ago

@omar-dulaimi https://docs.amplify.aws/start/q/integration/next

omar-dulaimi commented 3 years ago

Thank you @Danm72

flyandi commented 3 years ago

This is awesome but the question is now, how do I use amplifyhosting to enable SSR? :-)

ianmartorell commented 3 years ago

This is awesome but the question is now, how do I use amplifyhosting to enable SSR? :-)

@flyandi That's tracked in this issue: https://github.com/aws-amplify/amplify-console/issues/412

ohlr commented 3 years ago

One question regarding the blog

It suggests using api-keys to authorize api-access during the build of the app. If I do that, my builds stop working as soon the key expires (after 7 days).

Is there any solution to this? @ericclemmons

I tried IAM but without success.

ericclemmons commented 3 years ago

@ohlr For real-world apps, I also opt for IAM instead. (Originally, the tutorial used IAM but it has a few more prompts that are easy to miss)

When you run amplify add api or amplify update api and select IAM, but be sure to set the Create/Read/Update/Delete permissions correctly. You'll want the IAM account to read-only, and leave Cognito User Pools to the write/delete operations.

ohlr commented 3 years ago

@ericclemmons Thank you for the hint with the permissions. I previously did not configure iam as provider.

      #schema.graphql
      { allow: groups, groups: ["Admin"] }
      { allow: public, operations: [read], provider: iam }
ericclemmons commented 3 years ago

@ohlr Of course! I should've mentioned that or shared this link:

https://docs.amplify.aws/cli/graphql-transformer/auth#public-authorization

hugomn commented 3 years ago

Thanks for the great work adding SSR to amplify! I use apollo-client to handle our graphql queries and state, so I cannot use the SSR.API.graphql() call. I'd need to get the session jwt token on the server and create an apollo client with it. Is it currently possible? I tried something like below, without sucess.

export const getServerSideProps: GetServerSideProps = async (context) => {
  const { Auth } = withSSRContext(context)
  const session = await Auth.currentSession()
  const token = session.idToken.jwtToken
  console.log('Apollo client token: ', token)
  return {}
}

Any ideas? Thanks!

MontoyaAndres commented 3 years ago

I'm trying to execute Auth.signOut but sometimes the user is logout and sometimes not. This is what I'm doing:

  const signOut = () => {
    Auth.signOut().then(() => {
      router.reload();
    });
  };

When the user executes the function signOut, the page is reloaded, then the /page/MySecretPage.tsx executes this method to validate if the user has access or not:

export async function getServerSideProps(context) {
  await withAuth(context);

  return {
    props: {},
  };
}

This is the function withAuth:

import { withSSRContext } from 'aws-amplify';

export const withAuth = async context => {
  try {
    const SSR = withSSRContext(context);
    const user = await SSR.Auth.currentAuthenticatedUser();

    return user;
  } catch (error) {
    context.res.writeHead(301, { Location: '/signin' });
    context.res.end();
  }
};

What can I do? :/

dbhagen commented 3 years ago

Wow, I'm just hitting the same issue, @MontoyaAndres! Amazing timing. Out of curiosity, are you using SSO of any kind?

MontoyaAndres commented 3 years ago

@dbhagen I'm not the only one, that's great :), yes, I'm using sso. I tried to combine it with SSR but it didn't work, also sometimes the method on the function getServerSideProps does not work, and throw me to login...