Rachèl Heimbach
Full stack dev @ OpenValue
Lots of endpoints
HTTP caching
Full resources
Versioning
One endpoint
Client caching
Specific fields
No versioning
What does this mean?
Frontend should understand backend resource performance
Backend should understand frontend reqs
Shared responsibility performance
github.com/rachnerd/webshop
Branch: codemotion-asssignment-1
Server-side
const resolvers: GQLResolvers<ServerContext> = {
Query: {
items: (_, __, { itemsService }) => {
console.debug("Queried items");
return itemsService.get();
}
},
Item: {
price: (item, _, { pricesService }) => {
console.debug("Queried item price");
return pricesService.getById(item.id);
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: (): ServerContext => ({
itemsService: new ItemsService(),
pricesService: new PricesService()
}),
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
N+1 Problem
const resolvers: GQLResolvers<ServerContext> = {
Query: {
items: (_, __, { itemsService }) => {
console.debug("Queried items");
return itemsService.get();
}
},
Item: {
price: (item, _, { pricesService }) => {
console.debug("Queried item price");
return pricesService.getById(item.id);
}
}
};
{
items {
id
title
description
image
category
price
}
}
Queried items Queried item price Queried item price Queried item price ...
20 items:
export class PricesService {
// ...
async getById(id: number): Promise<number> {
return fetch(`${PRICES_ENDPOINT}/${id}`)
.then((res) => res.json());
}
// ...
}
const resolvers: GQLResolvers<ServerContext> = {
Query: {
items: (_, __, { itemsService }) => {
console.debug("Queried items");
return itemsService.get();
}
},
Item: {
price: (item, _, { pricesService }) => {
console.debug("Queried item price");
return pricesService.getById(item.id);
}
}
};
input 1
input 2
input 3
request
response
output 2
output 3
output 1
export class PricesService {
private priceDataLoader = new DataLoader<number, number>(
async (ids: number[]) =>
ids.length === 1
? [
await fetch(`${PRICES_ENDPOINT}/${ids[0]}`).then((res) =>
res.json()
),
]
: this.getByIds(ids)
);
async getById(id: number): Promise<number> {
return this.priceDataLoader.load(id);
}
async getByIds(ids: number[]): Promise<number[]> {
return fetch(`${PRICES_ENDPOINT}?ids=${ids.join(",")}`)
.then((res) => res.json());
}
}
20 items:
const resolvers: GQLResolvers<ServerContext> = {
Query: {
items: (_, { params }, { itemsService }) => {
console.debug("Queried items");
return itemsService.get(params);
}
},
// ...
};
export class ItemsService {
// ...
async get({ page, size }: GQLItemsParams): Promise<GQLPagedItems> {
return fetch(
`${BACKEND}/api/items?page=${page}&size=${size})}`
).then((res) => res.json());
}
// ...
}
DEMO
Client-side
/**
* Fetch remote items to display.
*/
const itemsQuery = useQuery<Paged<RemoteItem>>("items", () =>
getItemsRequest({ paging: PAGING })
);
/**
* Fetch remote cart information.
*/
const cartQuery = useQuery<RemoteCartItem[]>("cart", () =>
getCartRequest()
);
/**
* Keeping items as local state
*/
const [items, setItems] = useState<ClientItem[] | undefined>(undefined);
/**
* Aggregation logic that populates items with `amountInCart`
* RemoteItem -> ClientItem
*/
useEffect(() => {
const remoteItems = itemsQuery.data?.content;
const cart = cartQuery.data;
if (remoteItems && cart) {
const normalizedCart = normalize(cart);
const clientItems = remoteItems.map((item) => ({
...item,
amountInCart: normalizedCart[item.id]?.quantity || 0,
}));
setItems(clientItems);
}
}, [itemsQuery.data, cartQuery.data]);
export default function Index() {
const itemsQuery = useItemsQuery({
variables: {
size: 6,
page: 0,
},
});
// ...
return (
<HomePage
itemsState={{
loading: fastItemsQuery.loading,
data: itemsQuery.data?.items.content,
error: fastItemsQuery.error,
}}
/>
)
}
query Items($page: Int!, $size: Int!) {
items(params: { page: $page, size: $size }) {
content {
id
title
description
image
category
price
amountInCart
}
page
size
totalResults
}
}
DEMO
query FastItems($page: Int!, $size: Int!) {
items(params: { page: $page, size: $size }) {
content {
id
title
description
category
image
}
}
}
query SlowItems($page: Int!, $size: Int!) {
items(params: { page: $page, size: $size }) {
content {
id
price
amountInCart
}
}
}
export default function Index() {
const [items, setItems] = useState<ClientItem[]>([]);
const fastItemsQuery = useFastItemsQuery({
variables: PAGING,
});
const slowItemsQuery = useSlowItemsQuery({
variables: PAGING,
});
const fastItems = fastItemsQuery.data;
const slowItems = slowItemsQuery.data;
useEffect(() => {
if (fastItems && slowItems) {
const slowUpdates = normalize(slowItems.items.content);
const combinedItems = fastItems.map((item) => ({
...item,
...(slowUpdates[item.id] || {}),
}));
setItems(combinedItems);
}
}, [fastItems, slowItems]);
// ...
}
await addToCartMutation({
variables: { id, quantity },
update: (cache) =>
cache.writeFragment({
id: `Item:${id}`,
fragment: gql`
fragment UpdatedItem on Item {
amountInCart
}
`,
data: {
amountInCart: quantity,
},
}),
});
await addToCartMutation({
variables: { id, quantity },
refetchQueries: ['SlowItems'],
});