Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programmatic API #291

Closed
rauchg opened this issue Nov 23, 2016 · 30 comments
Closed

Programmatic API #291

rauchg opened this issue Nov 23, 2016 · 30 comments
Assignees

Comments

@rauchg
Copy link
Member

rauchg commented Nov 23, 2016

Moving forward, it will be possible to substitute next start in your programs with your own script, initialized without any wrappers. For example, node index.js.

An example of such a script in its most basic form would be as follows:

const next = require('next')
const { createServer } = require('http')

// init next
const app = next(/* __dirname */) 
const handle = app.getRequestHandler()

// init normal http server
createServer(handle).listen(3000)

This basically does nothing interesting, it's the equivalent of next -p 3000.

An important observation: notice that the parameter passed to next() to initialize the app is not a config object, but a path. This is because the configuration will be conventionally located in a next.config.js file where a next project lives. Credit goes to @Compulves (#222) for this great contribution.

This is crucial because even though we get to substitute the server code, we don't break existing and future useful next sub-commands. Especially next build, which makes deployment to production of your next apps so convenient, continues to work well even with both node server.js and next start.

Custom routing

Next.js's pages/ serves two purposes: to give you a super easy-to-use API for defining routes, but more importantly, to define different entry points for code splitting!

I say more importantly because it should be (and will be) possible to completely override the default route mapping, by invoking next/render manually.

The following example shows how you could point /a to pages/b.js and /b to pages/a.js, effectively changing the default routing behavior

const next = require('next')
const render = require('next/render')
const { createServer } = require('http')
const { parse } = require('url')

// init next
const app = next(/* __dirname */) 
const handle = app.getRequestHandler()

// init normal http server
createServer((req, res) => {
  const { query, pathname } = parse(req.url, true)
  if ('/a' === pathname) {
    render(app, res, 'b', qry)
  } else if ('/b' === pathname) {
    render(app, req, res, 'a', query)
  } else {
    handle(req, res)
  }
}).listen(3000)

In addition to the render method, we'll expose lower level render APIs, like renderToHTML and renderError, which work like render but don't do the automatic response handling. Those APIs are useful for building a caching layer, for example, which is really handy if you know that a certain component will always return the same HTML with given props.

More complicated routes like /posts/:id/tags or even /:anything can be handled by just parsing the URL and passing the appropriate props as the last parameter of render (as seen in the example), by using something like path-match

This is an example of how you do a catch all route, so that every request to /catch/* goes to a page pages/my/page.js:

const next = require('next')
const render = require('next/render')
const { createServer } = require('http')
const { parse } = require('url')

// init next
const app = next(/* __dirname */) 
const handle = app.getRequestHandler()

// set up routes
const match = require('path-match')()
const route = match('/catch/:all')

// init normal http server
createServer((req, res) => {
  const { query, pathname }  = parse(req.url, true)
  const params = match(pathname);
  if (false === params) {
    // handle it with default next behavior
    handle(req, res)
  } else {
    // renders `pages/my/page.js`
    render(app, req, res, 'my/page', query)
  }
}).listen(3000)

The idea here is that we retain the smallest possible API surface, minimize the number of dependencies you have to install and code to evaluate.

Fancy routing approaches vary widely, and we don't want to lock you in to a particular one. It should be entirely possible to implement express instead of http.createServer, for example.

@rauchg
Copy link
Member Author

rauchg commented Nov 23, 2016

Important: since we make requests to get a JSON representation of the components for lazy loading, we'll be switching those to Accept-Encoding instead of a .json suffix. That should make server code easier to write, as well.

@Francisc
Copy link

The priority tag is perfect.
Thank you.

@pemrouz
Copy link

pemrouz commented Nov 28, 2016

How does this work client-side? When you click a link, do you have to go back to the server to see what the following block evaluates to?

createServer((req, res) => {
  const { query, pathname } = parse(req.url, true)
  if ('/a' === pathname) {
    render(app, res, 'b', qry)
  } else if ('/b' === pathname) {
    render(app, req, res, 'a', query)
  } else {
    handle(req, res)
  }
})

@rauchg
Copy link
Member Author

rauchg commented Dec 1, 2016

@pemrouz the client makes the href explicit, and can decorate optionally the URL with as.

<Link href="/user?id=3" as="/pemrouz">

this is so that we can cache and pre-fetch components more effectively :)

@wookiehangover
Copy link

wookiehangover commented Dec 2, 2016

Very excited to see this take shape! One question about the proposed render method:

// renders `pages/my/page.js`
render(app, req, res, 'my/page', query)

I'm wondering if this could be done without passing req and res to render.

Seems like separating the concerns of rendering and handling the request would be useful — boiling it down to a render method that only provides the output of the render, leaving the HTTP response up to the caller.

Something more like this:

render(app, 'my/page', query)
  .then(content => {
    res.send(content)
  })

@rauchg
Copy link
Member Author

rauchg commented Dec 2, 2016

@wookiehangover that's the case: #310

@wookiehangover
Copy link

wookiehangover commented Dec 2, 2016 via email

@rauchg
Copy link
Member Author

rauchg commented Dec 2, 2016

The dependence on those objects is that getInitialProps requires them for distinguishing between server and client for example

@luisrudge
Copy link

will the as prop work in both the client and the server automagically?

@rauchg
Copy link
Member Author

rauchg commented Dec 5, 2016

@luisrudge as long as the underlying <a> is output with the as value in href, everything should work out of the box. For example, command+click (open in new tab), would go the as.

But that goes for both server and client. The server doesn't really "care" about as other than that detail I described

@amannn
Copy link
Contributor

amannn commented Dec 5, 2016

I think that could work out well. I was initially unsure, if this implies having the url mappings duplicated across every link. But actually this can be solved through composition: E.g. with a global configuration that is put on context and a custom Link wrapper like:

{
  users: {
    url: '/users',
    page: '/users'
  },
  userDetails: {
    url: '/users/{userId}',
    page: '/userDetails'
  }
}

<CustomLink to="userDetails" params={{userId: '123'}}>User</CustomLink>

could render:

<Link href="/users/123" as="/users">User</Link>

Something like this should be possible, right?

@rauchg
Copy link
Member Author

rauchg commented Dec 8, 2016

indeed.

@randallb
Copy link

randallb commented Dec 8, 2016

Is a PR wanted for this or is this more internal team stuff since it's more core?

@DarrylD
Copy link

DarrylD commented Dec 8, 2016

Is it safe to assume once this get's released, most will default to express? Maybe we should gear the examples for the case? I foresee many issues asking how to get it working with express.

Also wondering if a PR was needed.

@chr1shaefn3r
Copy link

chr1shaefn3r commented Dec 8, 2016

@randallb @DarrylD There is already a PR open for this bug: #310
And there is one for an express example: #352

@corysimmons
Copy link
Contributor

@DarrylD FWIW I'm thinking of using it with WP API. I'm a newb so excuse me if this is stupid/wrong/irrelevant.

@nickredmark
Copy link

Will it be possible to not use the filesystem-based router at all, and still have code splitting for each route?
For example, if I map /post/:id to /components/post.js will webpack still serve only the necessary code?

@zackify
Copy link

zackify commented Dec 16, 2016

It would be cool to do some sort of built in index page. For instance /blog/coding contains many posts, if I make an index.js, automatically returning a list of every page in this directory and paginate it would be useful. I imagine many people would use this.

@rauchg
Copy link
Member Author

rauchg commented Dec 16, 2016

@nmaro that's exactly the case. All that changes is the "look" of the URL. Think of the pages as webpack entry points.
@zackify we won't do anything magical like that, everything will be explicit. You should be able to pull that off with base next

@zackify
Copy link

zackify commented Dec 16, 2016 via email

@babakness
Copy link

Sample project making use of Programmatic API would be nice. When I try to require('next') directly from index.js it fails. Looks like there is some build process that needs to take place first? Please clarify how this works.

@babakness
Copy link

Yeah so

npm init -y
yarn add next
yarn add random
node
> require('next')
Error: Cannot find module 'next'
> require('random')
{ generateIntegers: [Function],
  generateSequence: [Function],
  generateStrings: [Function],
  checkQuota: [Function] }

So how does the home page example for "custom routing" work? How do you directly require next? Do I ned to configure babel / webpack or something? Seems like custom routing should still have nice things out of the box.

Please clarify.

@webyak
Copy link

webyak commented Dec 22, 2016

@babakness If I see it correctly the latest changes that include the programmatic api haven't been published to npm yet

@sedubois
Copy link
Contributor

@babakness @webyak install the 2.0 beta: npm i -S next@beta

@babakness
Copy link

@webyak @sedubois Ok, thanks. So now it loads but <style jsx> tags don't seem to get applied at all. Thoughts?

@rohmanhm
Copy link
Contributor

I feel confused on this line
const app = next(/* __dirname */)
__dirname for what?

@possibilities
Copy link
Contributor

@rohmanhm I'd consider that a "sketch". The final API is documented here https://github.com/zeit/next.js#custom-server-and-routing

@rohmanhm
Copy link
Contributor

rohmanhm commented Mar 28, 2017

@possibilities
Look this image from README Documentation
image

I'm confused about this line
next(path: string, opts: object) - path is

path is not defined

@possibilities
Copy link
Contributor

Looks like a typo, if you go back it time it used to say:

  • next(path: string, opts: objecvt) - path is where the Next project is located

https://github.com/zeit/next.js/blame/b337433d14435ef2fc4d193af5c2e7c9ec552583/README.md#L311

@abachuk
Copy link

abachuk commented May 27, 2017

It's still not very clear how to add custom middleware like API proxy and not just render components based on the path.

server.use('/api', myCustomMiddleware);

thanks.

@lock lock bot locked as resolved and limited conversation to collaborators May 11, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests