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

Feature: custom proxy settings #208

Closed
yianL opened this issue May 24, 2016 · 43 comments
Closed

Feature: custom proxy settings #208

yianL opened this issue May 24, 2016 · 43 comments

Comments

@yianL
Copy link

yianL commented May 24, 2016

First of all, great work with react-story book! I love how I can now jump between different states of my components quickly, and work on the visual changes.

So I have a component that fetches data from an api, and plots it onto a chart. Although I can write a story and feed mocked data into the component, sometimes it's helpful to render the "smart" version of the component and see it in real action. Problem is, I used to setup my own express server with webpackDevMiddleware, and then configure proxies so I can redirect api calls to another port. I couldn't find a way to setup similar configuration when using react-storybook. I ended up with a hack by going into node_modules/.bin/start-storybook and put my proxy settings there.

I'm not sure if this is a use case the authors intended to support, but I think it'll be really nice if I can use react-storybook as a playground for all sorts of components, whether they're dumb or smart. To achieve that, we would need a way to support custom proxy settings.

@thani-sh
Copy link
Contributor

Hi @yianL,

Thanks for bringing this use case to our attention. I started working on a way to make storybook more flexible here.

@thani-sh
Copy link
Contributor

Published v1.28.0. You can now import the middleware and write your own express server.

import storybook from '@kadira/storybook/dist/server/middleware';

// ...
app.use(storybook(configDirectory));
// ...

Check this line

@yianL
Copy link
Author

yianL commented May 25, 2016

Wow, this is awesome. I will definitely try it out. Feel free to close this issue.

Update: it worked perfectly. This really adds much flexibility to how you can setup your storybook. Nice!

@tornglintaffychen
Copy link

@yianL do you mind sharing the config sample you are using that works with storybook?

@ndelangen
Copy link
Member

@maxww What kind of example are you looking for?
Something like this:
https://github.com/mthuret/storybook-addon-specifications/blob/jest-test-files/.storybook/middleware.js ?

@ndelangen
Copy link
Member

We should document this ability better I think.

@tornglintaffychen
Copy link

@ndelangen I don't have server side and one of the components is using xhr to make ajax call get fetch more data. I want to write the webpack.config to be able to do like

proxy: {
     '/pws': 'http://www.someline.com'
}

I've tried it to be inside and outside the devSever object, no luck.
Since I'm fairly new to this, I'm not sure if I was even doing this right.

@yianL
Copy link
Author

yianL commented Jun 1, 2017

@maxww Here's a sample of my setup:

var Express = require('express')
var proxy = require('http-proxy-middleware')
var storybook = require('@kadira/storybook/dist/server/middleware').default
var app = new Express()
var port = 4001
var storybookConfig = './.storybook'

app.use(storybook(storybookConfig))

app.use('/api', proxy({
  target: 'https://api-endpoint', // target host
  changeOrigin: true,
}))

app.listen(port, function (error) {
  if (error) {
    console.error(error)
  } else {
    console.info('==> 🌎  Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port)
  }
})

Let me know if you have any questions.

Oh btw, this worked for me in storybook v1.28.1, and I haven't bumped the version in a long time. Not sure if anything has changed since then.

@ndelangen
Copy link
Member

I have to say that's some creative usage, but I cannot recommend anyone to use files deep into an package.

We cannot and will not guarantee those files will stay in those location or contain the same exports. It's not our API.

Including storybook inside an express app is a use case ew haven't seen much so far; though I'm interested in supporting it officially at some point.

@maxww storybook HAS a 'backend' and it's express. By adding a middleware.js in your storybook config folder, you can inject any route you want.

I guess you can try and do the same inside a webpack config, I don't know I've never tried that.
Let me know when you pull it off, if that works it'd be great to document it how.

@ndelangen
Copy link
Member

I don't understand how you say you don't have a backend, but you component is fetching data somewhere?

Do you mean you're building storybook to a static site?

@indiesquidge
Copy link

indiesquidge commented Jun 7, 2017

UPDATE: I see now in #445 that proxy configuration with Webpack was requested at some point and not recommended. I also see that there was once a local database add-on, but it appears to be discontinued (although it does look as though parts of it may be integrated into this repository).

I'm still a bit confused as to what is working and what is not, what is recommended, what is supported, etc. when it comes to proxying. It seems there is sparse documentation for using the middleware as well. I'd love to help develop and document this as soon as I have a better understanding of what currently exists and in what direction the team plans to move this feature with Storybook v3.


I'm not sure if this is what @maxww was getting at, but I'm wondering about a similar situation in which I "don't have a backend", meaning that I am calling an external API for my data.

TL;DR

Is there a way for me to initialize Storybook with an API proxy without having a local Express server or webpack.config.js to customize myself?

Initializing Storybook with an API Proxy

I am using Create React App for my React project now, and would love to find a way in which I can still call the API endpoints when using Storybook.

CRA makes use of a proxy property in the package.json to allow for proxying API requests in development. Something similar to what @maxww posted.

(package.json)

"proxy": "https://api-endpoint.com"

⚠️ Note: CRA does not expose a webpack config file directly, as one of it's core philosophies is "no configuration required". Because of this, writing custom webpack config is not an option unless one ejects from CRA, which may be unavoidable once a team wants a more granular level of control and customization, but is not recommended unless absolutely needed since it is irreversible.

Ideally, I would love to keep this working with isolated components in Storybook, so that things like an autocomplete search form can be properly used, styled, and tested.

I currently have this working by using Storybook environment variables. (As an aside, I don't know if the documentation for environment variables is still actively being kept up-to-date, as I only found it via a Google search, and it does not appear anywhere as a clickable link within the Storybook column nav panel in the official documentation.)

const apiUrl = process.env.REACT_APP_API_URL || process.env.STORYBOOK_API_URL

export const searchSuggestions = fragment =>
  fetch(`${apiUrl}/autocomplete/v1/suggestions/${fragment}`)

I then start Storybook like

STORYBOOK_API_URL="https://api-endpoint.com" yarn storybook

And now I can make calls the the API endpoints using the same proxy method that Create React App allows me to use.

However, this feels a bit hacky, and goes against what the Storybook environment variables docs recommend

Even though we can access these env variables anywhere in the client side JS code, it’s better to use them only inside stories and inside the main Storybook config file.

Is it possible to initialize Storybook with an API proxy without having to run my own Express server or editing the webpack config directly? Perhaps some form of middleware in the Storybook setup?

If this is not possible, then are there any foreseeable repercussions with the current method I have set up that uses a Storybook environment variable?

@ndelangen
Copy link
Member

@indiesquidge wanna do a 1 on 1 talk about thi topic in a sky call or something? talk to me on slack 👍

@indiesquidge
Copy link

indiesquidge commented Jun 7, 2017

@ndelangen, just when I saw your message I was able to find a solution to my problem using the awesome middleware customization roughly outlined in #435.

It is a very similar solution to @yianL's solution above, only I am not creating my own Express server and instead leveraging Storybook's router handler in the middleware.

I have the following in .storybook/middleware.js

const proxy = require('http-proxy-middleware')

module.exports = function expressMiddleware (router) {
  router.use('/api', proxy({
    target: 'https://api-endpoint.com',
    changeOrigin: true
  }))
}

This has a dependency on the http-proxy-middleware package.

It uses a proxy function to map a context ("/api") to a target ("api-endpoint.com"). One thing to note is that the changeOrigin option must be explicitly set to true in order for the origin of the host header to be changed to the target URL. The proxy function can be used for any number of proxies.

Thank y'all so much for exposing the middleware custom config in Storybook, that is a very helpful feature indeed 💛

@shilman
Copy link
Member

shilman commented Jun 8, 2017

@indiesquidge can you explain your workflow a bit more? so you have stories for container-style components that are end-to-end connected to your live API? Or do your stories grab the data from the server and then use it populate your components? Do you have any public examples of stories like this I can look at, either in a repo or just comment here or create a gist? Super curious.

@indiesquidge
Copy link

indiesquidge commented Jun 8, 2017

you have stories for container-style components that are end-to-end connected to your live API

Yeah, this is more inline with what I am working with.

Unfortunately I don't have a public example to show, but the main component that was causing me grief was an <AutocompleteForm /> component that makes a request to a backend API for autocomplete suggestions based on the user's query fragment on every keypress. The suggestions are stored in the component state, and I am using the API to update the state inside of a component method.

Something like this

export default class AutocompleteForm extends Component {
  state = {
    fragment: '',
    suggestions: []
  }

  handleSearch = fragment => {
    return api
      .searchSuggestions(fragment)
      .then(api.validateResponse)
      .then(api.toJSON)
      .then(json => json.suggestions.map(s => s.term))
      .then(suggestions => this.setState(() => ({ suggestions })))
      .catch(() => this.setState(() => ({ suggestions: [] })))
  }

  onChange = e => {
    const fragment = e.target.value
    this.handleSearch(fragment)
    this.setState(() => ({ fragment }))
  }

  render () {
    const { fragment, suggestions } = this.state

    return (
      <form>
        <label htmlFor='product-search'>Search</label>
        <input id='product-search' value={fragment} onChange={this.onChange} />
      </form>
      <ul>
        {suggestions && suggestions.map((suggestion, i) => <li key={i}>{suggestion}</li>}
      </ul>
    )
  }
}

The real component is a bit more complex, but this is the gist of it. I'd like to be able to show the suggestions list of <AutocompleteForm /> in the story. The story is as simple as just rendering this component.

api.searchSuggestions looks similar to how I wrote it in my first comment, only I'm not using Storybook environment variables anymore.

const apiUrl = process.env.REACT_APP_API_URL || ''

export const searchSuggestions = fragment =>
  fetch(`${apiUrl}/api/autocomplete/v1/suggestions/${fragment}`)

@shilman
Copy link
Member

shilman commented Jun 9, 2017

@indiesquidge Thanks for sharing! I think this would make an awesome blog post once you're happy with a solution. Would be thrilled to publish it on Storybook's Medium publication if that's at all interesting: https://medium.com/storybookjs

@indiesquidge
Copy link

@shilman, that sounds great! I'll work on something this week and get back to you soon :)

@djyde
Copy link

djyde commented Aug 3, 2017

It's really a useful feature, since the webpack-dev-server has a proxy config out of the box. Though we should do this work manually, but why there is no document for it? I don't even know the middle ware can be override by adding a middleware.js.

I am willing to create a PR for this part of doc.

@ndelangen
Copy link
Member

A PR on our docs to improve the information regarding middleware.js would be fantastic!

@djyde
Copy link

djyde commented Aug 3, 2017

I will work on it. 😋

@hmontes
Copy link

hmontes commented Aug 17, 2017

Any news? I want to connect Apollo Graphql and i need this option (Like proxy config in create-react-app).

Thank you :)

@djyde
Copy link

djyde commented Aug 18, 2017

@hmontes This is how I did:

// .storybook/middleware.js

const proxy = require('http-proxy-middleware')
const packageJson = require('../package.json')

module.exports = function expressMiddleware(router) {
  const proxyConfig = packageJson.proxy || {}

  for (let domain in proxyConfig) {
    if (typeof proxyConfig[domain] === 'string') {
      console.log(domain)
      router.use(domain, proxy({
        target: proxyConfig[domain]
      }))
    } else {
      router.use(domain, proxy(proxyConfig[domain]))
    }
  }
}
// package.json

{
  "proxy": {
    "/api": "http://localhost:5000/api"
  }
}

@adamchenwei
Copy link

anyone would add the doc for allowing webpack dev server proxy into the storybook?

@adamchenwei
Copy link

adamchenwei commented Dec 29, 2017

this should add into the doc...
from @indiesquidge

const proxy = require('http-proxy-middleware')

module.exports = function expressMiddleware (router) {
  router.use('/api', proxy({
    target: 'https://api-endpoint.com',
    changeOrigin: true
  }))
}

@chrbala
Copy link

chrbala commented Jan 3, 2018

To make it API-compatible with CRA, the following works:

const proxy = require('http-proxy-middleware');

const { proxy: proxyConfigs } = require('../package.json');

module.exports = router =>
  Object.keys(proxyConfigs).forEach(key =>
    router.use(key, proxy(proxyConfigs[key]))
  );

IMO this should be added as a default to storybook, since it does say that it bases the default setup on CRA.

@adamchenwei
Copy link

@chrbala what is CRA?

@picheli20
Copy link

picheli20 commented Mar 13, 2019

Who's using Angular, can use what @djyde did but simply use the .storybook/middleware.js and use your current proxy.config.json:

const proxy = require('http-proxy-middleware')
const proxyConfig = require('../proxy.conf.json')

module.exports = function expressMiddleware(router) {

  Object.keys(proxyConfig).forEach(domain => {
    const target = proxyConfig[domain];
    if (typeof target === 'string') {
      console.log(domain)
      router.use(domain, proxy({ target }))
    } else {
      router.use(domain, proxy(proxyConfig[domain]))
    }
  });
}

@dereklin
Copy link

I am using Angular. Is there a way to do proxy per story instead of per the whole storybook?

@picheli20
Copy link

@dereklin you can add some prefix for earch story on the API request and re-write it on the proxy it-self :)

@dtslvr
Copy link

dtslvr commented Oct 9, 2019

Who's using Angular, can use what @djyde did but simply use the .storybook/middleware.js and use your current proxy.config.json:

const proxy = require('http-proxy-middleware')
const proxyConfig = require('../proxy.conf.json')

module.exports = function expressMiddleware(router) {

  Object.keys(proxyConfig).forEach(domain => {
    const target = proxyConfig[domain];
    if (typeof target === 'string') {
      console.log(domain)
      router.use(domain, proxy({ target }))
    } else {
      router.use(domain, proxy(proxyConfig[domain]))
    }
  });
}

Hi @picheli20

Thanks for the snippet. I have copied it to .storybook/middleware.js. In the terminal I can see [HPM] GET /api/v1/users/59b49fbc-022e-47af-8469-b88f9158a36b:2ac82679-f51a-4524-8684-13132d2a180d -> http://localhost:8888, but in DevTools > Network there is still GET http://localhost:6006/api/v1/users/59b49fbc-022e-47af-8469-b88f9158a36b:2ac82679-f51a-4524-8684-13132d2a180d (401 Unauthorized) .

Any ideas?

@picheli20
Copy link

@dtslvr and the same request to the backend works? Looks like that the request is going wrong, not the proxy

@dtslvr
Copy link

dtslvr commented Oct 9, 2019

@dtslvr and the same request to the backend works? Looks like that the request is going wrong, not the proxy

Wow, you're right 😀 The request is unauthorized because of a missing token. I was confused of the request to http://localhost:6006. Thanks a lot for your hint @picheli20.

@masood1
Copy link

masood1 commented Jul 15, 2020

@ndelangen, just when I saw your message I was able to find a solution to my problem using the awesome middleware customization roughly outlined in #435.

It is a very similar solution to @yianL's solution above, only I am not creating my own Express server and instead leveraging Storybook's router handler in the middleware.

I have the following in .storybook/middleware.js

const proxy = require('http-proxy-middleware')

module.exports = function expressMiddleware (router) {
  router.use('/api', proxy({
    target: 'https://api-endpoint.com',
    changeOrigin: true
  }))
}

This has a dependency on the http-proxy-middleware package.

It uses a proxy function to map a context ("/api") to a target ("api-endpoint.com"). One thing to note is that the changeOrigin option must be explicitly set to true in order for the origin of the host header to be changed to the target URL. The proxy function can be used for any number of proxies.

Thank y'all so much for exposing the middleware custom config in Storybook, that is a very helpful feature indeed 💛

Please use createProxyMiddleware, function is updated

const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api/login',
createProxyMiddleware({
target: 'http://localhost:3090',
changeOrigin: true,
})
);
};

@shilman
Copy link
Member

shilman commented Jul 16, 2020

FYI all of these APIs are experimental/undocumented, and we reserve the right to change/remove them outside of the normal rules of semver.

@teebodscvr
Copy link

Who's using Angular, can use what @djyde did but simply use the .storybook/middleware.js and use your current proxy.config.json:

const proxy = require('http-proxy-middleware')
const proxyConfig = require('../proxy.conf.json')

module.exports = function expressMiddleware(router) {

  Object.keys(proxyConfig).forEach(domain => {
    const target = proxyConfig[domain];
    if (typeof target === 'string') {
      console.log(domain)
      router.use(domain, proxy({ target }))
    } else {
      router.use(domain, proxy(proxyConfig[domain]))
    }
  });
}

This worked for me in an Angular project

@federico-ntr
Copy link

Please use createProxyMiddleware, function is updated

const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function(app) { app.use( '/api/login', createProxyMiddleware({ target: 'http://localhost:3090', changeOrigin: true, }) ); };

This did the trick for me, but it doesn't work if it's a TypeScript file. I had to rename it from middleware.ts to middleware.js. Am I missing something or should I open a new issue?

Note: I'm using Storybook 7.0.0-rc.5 with vite

@Evgeny-Chugaev
Copy link

Evgeny-Chugaev commented Jul 26, 2023

Works for me, "http-proxy-middleware": "^2.0.6",

const proxy = require('http-proxy-middleware')

module.exports = function expressMiddleware(router) {
  router.use('/api', proxy.createProxyMiddleware({
    target: 'https://your-site.here',
    changeOrigin: true
  }))
}

@ndelangen
Copy link
Member

I had to rename it from middleware.ts to middleware.js

Not a bug, it should be middleware.js exactly.
Improving the API isn't likely to happen, because of it's limitation that it can only work for development and not for build versions of storybook.

Long term we'd like to come up with a way to add serverside routes support to storybook that will be statically build.
But this API doesn't exist yet.

@biaogebusy
Copy link

biaogebusy commented Nov 23, 2023

working for me:

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function expressMiddleware(router) {
	router.use(
		"/sites",
		createProxyMiddleware({
			target: "domain.com",
			changeOrigin: true,
		})
	);
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests