Request client for the frontend
Get type-safety between your backend and frontend
Encore is able to generate frontend request clients (TypeScript or JavaScript). This lets you to keep the request/response types in sync without manual work and assists you in calling the APIs. Generate a client by running:
$ encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=<ENV_NAME>
Adding this as a script to your package.json
is often a good idea to be able to run it whenever a change is made to your Encore API:
{
...
"scripts": {
...
"generate-client:staging": "encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=staging",
"generate-client:local": "encore gen client <ENCORE-APP-ID> --output=./src/client.ts --env=local"
}
}
After that you are ready to use the request client in your code. In this example, the frontend is calling the GetNote
endpoint on the note
service in order to retrieve a specific meeting note (which has the properties id
, cover_url
& text
):
import Client, { Environment, Local } from "src/client.ts";
// Making request to locally running backend...
const client = new Client(Local);
// or to a specific deployed environment
// const client = new Client(Environment("staging"));
// Calling APIs as typesafe functions 🌟
const response = await client.note.GetNote("note-uuid");
console.log(response.id);
console.log(response.cover_url);
console.log(response.text);
See more in the client generation docs.
Asynchronous state management
When building something a bit more complex, you will likely need to deal with caching, refetching, and data going stale. TanStack Query is a popular library that was built to solve exactly these problems and works well with the Encore request client.
Here is a simple example of using an Encore request client together with TanStack Query:
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import Client, { todo } from '../encore-client'
// Create a Encore client
const encoreClient = new Client(window.location.origin);
// Create a react-query client
const queryClient = new QueryClient()
function App() {
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({
queryKey: ['todos'],
queryFn: () => encoreClient.todo.List()
})
// Mutations
const mutation = useMutation({
mutationFn: (params: todo.AddParams) => encoreClient.todo.Add(params),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
This example assumes that we have a todo
service with a List
and Add
endpoint. When adding the new todo,
TanStack Query will automatically invalidate the todos
query and refetch it.
For a real-world example, take a look at the Uptime Monitoring app which also makes use of
TanStack Query's refetchInterval
option for polling the backend.
Testing
When unit testing a component that interacts with your Encore API you can mock methods on the request client to return a value suitable for the test. This makes your test URL agnostic because you are not intercepting specific requests on the fetch layer. You also get type errors in your tests if the request client gets updated.
Here is an example from the Uptime Monitoring Starter where we are mocking a GET request method and spying on a POST request method:
import { render, waitForElementToBeRemoved } from "@testing-library/react";
import App from "./App";
import { site } from "./client";
import { userEvent } from "@testing-library/user-event";
describe("App", () => {
beforeEach(() => {
// Return mocked data from the List (GET) endpoint
jest
.spyOn(site.ServiceClient.prototype, "List")
.mockReturnValue(Promise.resolve({
sites: [{
id: 1,
url: "test.dev"
}]
}));
// Spy on the Add (POST) endpoint
jest.spyOn(site.ServiceClient.prototype, "Add");
});
it("render sites", async () => {
render(<App />);
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// Verify that the List endpoint has been called
expect(site.ServiceClient.prototype.List).toBeCalledTimes(1);
// Verify that the sites are rendered with our mocked data
screen.getAllByText("test.dev");
});
it("add site", async () => {
render(<App />);
await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
// Interact with the page and add 'another.com'
await userEvent.click(screen.getByText("Add website"));
await userEvent.type(
screen.getByPlaceholderText("google.com"),
"another.com",
);
await userEvent.click(screen.getByText("Save"));
// Verify that the Add endpoint has been called with the correct parameters
expect(site.ServiceClient.prototype.Add).toHaveBeenCalledWith({
url: "another.com",
});
});
})
Please note
In the example above we need to mock the List
method on site.ServiceClient.prototype
because the request client has not
yet been initialized when we're creating the mock. If you have access to the instance of the request client in your test
(which could be the case if you are passing the client around in your components) you can instead do jest.spyOn(client.site, "List")
and expect(client.site.List).toHaveBeenCalled()
which would give you the same result.
REST vs. GraphQL
Encore allows for building backends using both REST and GraphQL, you should pick the approach that suits your use case best. Encore's request client only works for REST APIs so if you choose to build a GraphQL backend you will need to use another request library for your frontend.
Take a look at the GraphQL tutorial for an example of building a GraphQL backend with Encore.
Related example
$ encore app create --example=ts/graphql