How To Build a REST directive with RedwoodJS Transformer Directives
Inspired by Build a REST directive with GraphQL Tools by Jaime Barton which you can watch here:
Jaime shows you how to build a custom @rest directive with GraphQL Tools to resolve data from a JSON API – but, we’ll use Redwood Directives instead to show how you can still implement this feature but not have to go deep into the GraphQL structure.
And … we’ll add a way to fetch a single item from a JSON API … and tests!
Live Demo!!!
Using a RedwoodJS Directive
Redwood Directives are a powerful feature, supercharging your GraphQL-backed Services.
You can think of directives like “middleware” that let you run reusable code during GraphQL execution to perform tasks like authentication and formatting.
Redwood uses them to make it a snap to protect your API Services from unauthorized access.
Here we call those types of directives Validators.
You can also use them to transform the output of your query result to modify string values, format dates, shield sensitive data, and more! We call those types of directives Transformers.
We’ll be using the Transformer Directive type to imoplemen the @rest
directive.
Generating the Directive
- yarn rw g directive rest --type=transformer
- rest-directive % yarn workspace api add cross-undici-fetch
Implement the SDL
We’ll use the https://jsonplaceholder.typicode.com
JSON api demo to get Photos and Posts.
You can browse the JSON response for https://jsonplaceholder.typicode.com/posts
to see how we match the SDL type Post
to the example data:
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
},
{
"userId": 1,
"id": 3,
"title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
"body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
},
...
]
For Posts
// api/src/graphql/posts.sdl.ts
export const schema = gql`
type Post {
id: Int!
title: String @uppercase
body: String
userId: String
}
type Query {
post(id: Int!): Post
@rest(url: "https://jsonplaceholder.typicode.com/posts/:id")
@skipAuth
posts: [Post]
@rest(url: "https://jsonplaceholder.typicode.com/posts")
@skipAuth
}
`
For Photos
// api/src/graphql/photos.sdl.ts
export const schema = gql`
type Photo {
id: Int!
albumId: Int!
title: String
thumbnailUrl: String
userId: String
}
type Query {
photo(id: Int!): Photo
@rest(url: "https://jsonplaceholder.typicode.com/photo/:id")
@skipAuth
photos: [Photo]
@rest(url: "https://jsonplaceholder.typicode.com/photos")
@skipAuth
}
`
Implement @rest directive
You can see the full @rest transformer directive implementation.
TLDR;
- It extracts the
url
set in the directive from itdirectiveArgs
- Extracts any query
args
to be used to replace the named parameters. - Replace those params if needed (this is for the fetch post by by query)
- Construct a url
- Fetch from this url
- Return the response
Testing
Yes, you can mock the JSON API response to test that your directive resturns the expected data.
See how the mockRedwoodDirective
testing utility let’s you pass in the directiveArtgs and args for the url with a mocked response:
// api/src/directives/rest/rest.test.ts
jest.mock('cross-undici-fetch', () => ({
fetch: (url) => {
switch (url) {
case 'https://example.com':
return {
ok: true,
json: async () => {
return POSTS_JSON
},
}
case 'https://example.com/1':
return {
ok: true,
json: async () => {
return POSTS_JSON[0]
},
}
}
},
}))
// ...
describe('demonstrate use of args for named parameter replacement', () => {
it('with a url for a single item, returns the json response for that json api url', async () => {
const mockExecution = mockRedwoodDirective(rest, {
mockedResolvedValue: '',
directiveArgs: { url: 'https://example.com/:id' },
args: { id: 1 },
})
await expect(mockExecution()).resolves.toEqual(POSTS_JSON[0])
})
})
See @rest Directive Unit Test Code
App
Now with these SDL and Directives you can query the RedwoodJS GraphQL API and use RedwoodJS cells to render Posts and photos – live examples!
See the web side code for this.
Ideas to Improve the @rest directive
- Add
headers
so can pass api tokens or other Authorization headers - Support
POST
andGET
methods - Chain with another Transformer directive to reshape the response (transform JSON API data to match a different SDL) (maybe?)
Motivation aka Why not just implement a service?
You may be wondering, why use this @rest
directive instead of implementing the same SDL and the implementing a service called posts
and post
and then in that service, use the same cross-unidici-fetch to fetch the url?
And the answer, is … I’d probably implement this type of feature using a service.
With a service, then a service can call another service (if some other feature needs a few posts to process)… or I could have a serverless function call the posts service to fetch for a RESTful API :).
I might even create a fetch client specifically for the JSON API in api/lib
.
But, for very simple REST API fetches one can save quite a bit of code by not having to implement a service method for each endpoint and each get many and get singular.
This Office Hours Example is of a showcase of the power and simplicity of creating Transformer Directives and how one can test them as well.
[Read More[(redwood-office-hours/README.md at main · redwoodjs/redwood-office-hours · GitHub)