Grafana version 7.x.

As of this writing, the tutorial from Grafana on how to write datasource plugin is completely broken. I wasted an entire afternoon trying to decipher what they were saying in the tutorial versus the rest of the documentation.

Executive summary: you need to proxy your requests through a route configuration in your plugin json. Otherwise you will still get hit by CORS even though the tutorial says you're using the Grafana server thread to send requests.

Following the tutorial, you would think it would be as simple as adding your URL to the call:

async doRequest(query: MyQuery) {
    const result = await getBackendSrv().datasourceRequest({
      method: 'GET',
      url: 'http://localhost:8000',
      params: query,
    });

    return result;
  }

This REST call is dirt simple. It's a python server that returns some BS nonsense:

from http.server import HTTPServer, BaseHTTPRequestHandler

from io import BytesIO

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    message_rec = 0

    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'[{"time": "time", "value": "value"}]')

httpd = HTTPServer(('localhost', 8000), SimpleHTTPRequestHandler)
httpd.serve_forever()

As written in the Grafana tutorial, you would expect this to work. They magically route the call through the server backend. This is not how it works. You are still making a REST call from the frontend javascript that the browser is rightfully blocking.

You would need to stumble across this documentation.

Ironically, the documentation points this out, but without a citation and without clarification that it is all external requests, even ones that do not need auth:

The main advantage of getBackendSrv is that it proxies requests through the Grafana server rather making the request from the browser. This is strongly recommended when making authenticated requests to an external API. For more information on authenticating external requests, refer to [Add authentication for data source plugins].

There is an API endpoint you need to hit, to "proxy" your request through the Grafana backend to your actual external endpoint. Grab the "url" out of the constructor of your instance settings like this:

export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {

  url?: string;

  constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
    super(instanceSettings);
    this.url = instanceSettings.url;
  }
  
  async doRequest(query: MyQuery) {
    const result = await getBackendSrv().datasourceRequest({
      method: 'GET',
      url: this.url + '/example',
      params: query,
    });

    return result;
  }

You then need to update plugin.json to "route" this request through the backend proxy (routes is added at the top level):

  "routes": {
    "path": "example",
    "url": "http://localhost:8000"
  }

Reload grafana anytime you make a change to the plugin.json. It is not picked up automatically.

This magic will replace the path variable with the url, passing this request to your backend:

GET /api/datasources/proxy/:datasourceId/*

You'll then see data from your backend in your panel, et al.