Skip to content

Leveraging GraphQL Scalars to Enhance Your Schema

Leveraging GraphQL Scalars to Enhance Your Schema

Introduction

GraphQL has revolutionized the way developers approach application data and API layers, gaining well-deserved momentum in the tech world. Yet, for all its prowess, there's room for enhancement, especially when it comes to its scalar types. By default, GraphQL offers a limited set of these primitives — Int, Float, String, Boolean, and ID — that underpin every schema. While these types serve most use cases, there are scenarios where they fall short, leading developers to yearn for more specificity in their schemas.

Enter graphql-scalars, a library designed to bridge this gap. By supplementing GraphQL with a richer set of scalar types, this tool allows for greater precision and flexibility in data representation. In this post, we'll unpack the potential of enhanced Scalars, delve into the extended capabilities provided by graphql-scalars, and demonstrate its transformative power using an existing starter project. Prepare to redefine the boundaries of what your GraphQL schema can achieve.

Benefits of Using Scalars

GraphQL hinges on the concept of "types." Scalars, being the foundational units of GraphQL's type system, play a pivotal role. While the default Scalars — Int, Float, String, Boolean, and ID — serve many use cases, there's an evident need for more specialized types in intricate web development scenarios.

  1. Precision: Using default Scalars can sometimes lack specificity. Consider representing a date or time in your application with a String; this might lead to ambiguities in format interpretation and potential inconsistencies.
  2. Validation: Specialized scalar types introduce inherent validation. Instead of using a String for an email or a URL, for example, distinct types ensure the data meets expected formats at the query level itself.
  3. Expressiveness: Advanced Scalars provide clearer intentions. They eliminate ambiguity inherent in generic types, making the schema more transparent and self-explanatory.

Acknowledging the limitations of the default Scalars, tools like graphql-scalars have emerged. By broadening the range of available data types, graphql-scalars allows developers to describe their data with greater precision and nuance.

Demonstrating Scalars in Action with Our Starter Project

To truly grasp the transformative power of enhanced Scalars, seeing them in action is pivotal. For this, we'll leverage a popular starter kit: the Serverless framework with Apollo and Contentful. This kit elegantly blends the efficiency of serverless functions with the power of Apollo's GraphQL and Contentful's content management capabilities.

Setting Up the Starter:

  1. Initialize the Project:
npm create @this-dot/starter -- --kit serverless-framework-apollo-contentful
  1. When prompted, name your project enhance-with-graphql-scalars.
Welcome to starter.dev! (create-starter)
✔ What is the name of your project? … enhance-with-graphql-scalars
> Downloading starter kit...
✔ Done!

Next steps:
 cd enhance-with-graphql-scalars
 npm install (or pnpm install, yarn, etc)
  1. For a detailed setup, including integrating with Contentful and deploying your serverless functions, please follow the comprehensive guide provided in the starter kit here.
  2. And we add the graphql-scalars package
npm install graphql-scalars

Enhancing with graphql-scalars:

Dive into the technology.typedefs.ts file, which is the beating heart of our GraphQL type definitions for the project. Initially, these are the definitions we encounter:

export const technologyTypeDefs = gql`
	type Technology {
		id: ID!
		displayName: String!
		description: String
		url: URL
	}

	type Query {
		"Technology: GET"
		technology(id: ID!): Technology
		technologies(offset: Int, limit: Int): [Technology!]
	}

	type Mutation {
		"Technology: create, read and delete operations"
		createTechnology(displayName: String!, description: String, url: String): Technology
		updateTechnology(id: ID!, fields: TechnologyUpdateFields): Technology
		deleteTechnology(id: ID!): ID
	}

	input TechnologyUpdateFields {
		"Mutable fields of a technology entity"
		displayName: String
		description: String
		url: String
	}
`;

Our enhancement strategy is straightforward:

  • Convert the url field from a String to the URL scalar type, bolstering field validation to adhere strictly to the URL format.

Post-integration of graphql-scalars, and with our adjustments, the revised type definition emerges as:

export const technologyTypeDefs = gql`
	type Technology {
		id: ID!
		displayName: String!
		description: String
		url: URL
	}

	type Query {
		"Technology: GET"
		technology(id: ID!): Technology
		technologies(offset: Int, limit: Int): [Technology!]
	}

	type Mutation {
		"Technology: create, read and delete operations"
		createTechnology(displayName: String!, description: String, url: URL): Technology
		updateTechnology(id: ID!, fields: TechnologyUpdateFields): Technology
		deleteTechnology(id: ID!): ID
	}

	input TechnologyUpdateFields {
		"Mutable fields of a technology entity"
		displayName: String
		description: String
		url: URL
	}
`;

To cap it off, we integrate the URL type definition along with its resolvers (sourced from graphql-scalars) in the schema/index.ts file:

import { mergeResolvers, mergeTypeDefs } from '@graphql-tools/merge';
import { technologyResolvers, technologyTypeDefs } from './technology';
import { URLResolver, URLTypeDefinition } from 'graphql-scalars';

const graphqlScalars = [URLTypeDefinition];

export const typeDefs = mergeTypeDefs([...graphqlScalars, technologyTypeDefs]);

export const resolvers = mergeResolvers([{ URL: URLResolver }, technologyResolvers]);

This facelift doesn't just refine our GraphQL schema but infuses it with innate validation, acting as a beacon for consistent and accurate data.

Testing in the GraphQL Sandbox

Time to witness our changes in action within the GraphQL sandbox. Ensure your local server is humming along nicely.

Kick off with verifying the list query:

query {
  technologies {
    id
    displayName
    url
  },
}

Output:

{
  "data": {
    "technologies": [
      {
        "id": "4UXuIqJt75kcaB6idLMz3f",
        "displayName": "GraphQL",
        "url": "https://graphql.framework.dev/"
      },
      {
        "id": "5nOshyir74EmqY4Jtuqk2L",
        "displayName": "Node.js",
        "url": "https://nodejs.framework.dev/"
      },
      {
        "id": "5obCOaxbJql6YBeXmnlb5n",
        "displayName": "Express",
        "url": "https://www.npmjs.com/package/express"
      }
    ]
  }
}

Success! Each url in our dataset adheres to the pristine URL format. Any deviation would've slapped us with a format error.

Now, let's court danger. Attempt to update the url field with a wonky format:

mutation {
  updateTechnology(id: "4UXuIqJt75kcaB6idLMz3f", fields: { url: "aFakeURLThatShouldThrowError" }) {
    id
    displayName
    url
  }
}

As anticipated, the API throws up a validation roadblock:

{
  "data": {},
  "errors": [
    {
      "message": "Expected value of type \"URL\", found \"aFakeURLThatShouldThrowError\"; Invalid URL",
      "locations": [
        {
          "line": 18,
          "column": 65
        }
      ],
      "extensions": {
        "code": "GRAPHQL_VALIDATION_FAILED",
        "stacktrace": [
          "TypeError [ERR_INVALID_URL]: Invalid URL",
          "    at new NodeError (node:internal/errors:399:5)",
          "    at new URL (node:internal/url:560:13)",
          ...
        ]
      }
    }
  ]
}

For the final act, re-run the initial query to reassure ourselves that the original dataset remains untarnished.

Conclusion

Enhancing your GraphQL schemas with custom scalars not only amplifies the robustness of your data structures but also streamlines validation and transformation processes. By setting foundational standards at the schema level, we ensure error-free, consistent, and meaningful data exchanges right from the start.

The graphql-scalars library offers an array of scalars that address common challenges developers face. Beyond the URL scalar we explored, consider diving into other commonly used scalars such as:

  • DateTime: Represents date and time in the ISO 8601 format.
  • Email: Validates strings as email addresses.
  • PositiveInt: Ensures integer values are positive.
  • NonNegativeFloat: Guarantees float values are non-negative.

As a potential next step, consider crafting your own custom scalars tailored to your project's specific requirements. Building a custom scalar not only offers unparalleled flexibility but also provides deeper insights into GraphQL's inner workings and its extensibility.

Remember, while GraphQL is inherently powerful, the granular enhancements like scalars truly elevate the data-fetching experience for developers and users alike. Always evaluate your project's needs and lean into enhancements that bring the most value.

To richer and more intuitive GraphQL schemas!

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

How to Create a GraphQL Rest API Wrapper and Enhance Your Data cover image

How to Create a GraphQL Rest API Wrapper and Enhance Your Data

Intro Today we will talk about wrapping a REST API with a GraphQL wrapper, which means that the REST API will be accessible via a different GraphQL API. We’ll be using Apollo Server for the implementation. This article assumes you have a basic understanding of REST API endpoints, and some knowledge of GraphQL. Here is the code repo if you want to review it while reading. With that said, we will be looking at why you would wrap a REST API, how to wrap an existing REST API, and how you can enhance your data using GraphQL. Why wrap a REST API with GraphQL There are a couple of different reasons to wrap a REST API. The first is migrating from an existing REST API, which you can learn about in detail here, and the second is creating a better wrapper for existing data. Granted, this can be done using REST. But for this article, we will focus on a GraphQL version. A reason for creating a better wrapper would be using a CMS that provides custom fields. For instance, you get a field that is listed as C435251, and it has a value of 532. This doesn’t mean anything to us. But when looking at the CMS these values could indicate something like “Breakfast Reservation” is set to “No”. So, with our wrapping, we can return it to a more readable value. Another example is connecting related types. For instance, in the code repo for this blog, we have a type Person with a connection to the type Planet. __Connection example__* ` type Person { """The name of this person.""" name: String """A planet that this person was born on or inhabits.""" homeworld: Planet } type Planet { """The name of this planet.""" name: String } ` How to Wrap a REST API Alright, you have your REST API, and you might wonder how to wrap it with GraphQL? First, you will call your REST API endpoint, which is inside your rest-api-sources file inside your StarwarsAPI class. __REST API example__* ` class StarwarsAPI { constructor() { this.axios = axios.create({ baseURL: 'https://swapi.dev/api/', }); } async getPerson(id) { const { data } = await this.axios.get(people/${id}`); return data } async getHomeworld(id) { const { data } = await this.axios.get(planets/${id}`); return data } } ` This above class will then be imported and used in the server/index file to set up your new Apollo server. __Apollo server example__* ` const StarwarsAPI = require('./rest-api-sources/starwars-rest-api'); const server = new ApolloServer({ typeDefs, resolvers, dataSources: () => ({}), context: () => { return { starwarsAPI: new StarwarsAPI(), }; }, }); ` Now, in your GraphQL resolver, you will make a person query and retrieve your starWarsAPI from it, which contains the information you want to call. __GraphQL resolver__* ` const resolvers = { Query: { person: async (, { id }, { starwarsAPI }) => { return await starwarsAPI.getPerson(id); }, }, }; ` With the above done, let's start on how to enhance your data in the resolver. Enhancing your data With our resolver up and running, we’ll now use it to enhance some of our data. For now, we’ll make the name we get back returned in a first name, and the last initial format. To do so above our Query, we’ll start a Person object and put the variable name inside it. We’ll then grab the name from our Query and proceed to tweak it into the format we want. __Enhancing in resolver__* ` Person: { name: ({ name }) => { if (!name) { return null; } const [first, last] = name.split(" ") if (last === undefined) { return first } return ${first} ${last[0].toUpperCase()}.` } }, ` Tada! Now, when we call our GraphQL, our name will return formatted in a first name, and last initial state. Conclusion Today's article covered why you want to wrap a REST API with GraphQL for migration or to provide a better API layer, how to wrap an existing REST API with GraphQL, and how you can use the resolver to enhance your data for things like name formatting. I hope it was helpful, and will give others a good starting point. If you want to learn more about GraphQL and REST API wrappers, read up on our resources available at graphql.framework.dev....

How to Resolve Nested Queries in Apollo Server cover image

How to Resolve Nested Queries in Apollo Server

When working with relational data, there will be times when you will need to access information within nested queries. But how would this work within the context of Apollo Server? In this article, we will take a look at a few code examples that explore different solutions on how to resolve nested queries in Apollo Server. I have included all code examples in CodeSandbox if you are interested in trying them out on your own. Prerequisites** This article assumes that you have a basic knowledge of GraphQL terminology. Table of Contents - How to resolve nested queries: An approach using resolvers and the filter method - A refactored approach using Data Loaders and Data Sources - What are Data Loaders - How to setup a Data Source - Setting up our schemas and resolvers - Resolving nested queries when microservices are involved - Conclusion How to resolve nested queries: An approach using resolvers and the filter method In this first example, we are going to be working with two data structures called musicBrands` and `musicAccessories`. `musicBrands` is a collection of entities consisting of id and name. `musicAccessories` is a collection of entities consisting of the product name, price, id and an associated `brandId`. You can think of the `brandId` as a foreign key that connects the two database tables. We also need to set up the schemas for the brands and accessories. `graphql const typeDefs = gql scalar USCurrency type MusicBrand { id: ID! brandName: String } type MusicAccessories { id: ID! product: String price: USCurrency brandId: Int brand: MusicBrand } type Query { accessories: [MusicAccessories] } ; ` The next step is to set up a resolver for our Query` to return all of the music accessories. `js const resolvers = { Query: { accessories: () => musicAccessories, }, }; ` When we run the following query and start the server, we will see this JSON output: `graphql query Query { accessories { product brand { brandName } } } ` `json { "data": { "accessories": [ { "product": "NS Micro Violin Tuner Standard", "brands": null }, { "product": "Standard Gong Stand", "brands": null }, { "product": "Black Cymbal Mallets", "brands": null }, { "product": "Classic Series XLR Microphone Cable", "brands": null }, { "product": "Folding 5-Guitar Stand Standard", "brands": null }, { "product": "Black Deluxe Drum Rug", "brands": null } ] } } ` As you can see, we are getting back the value of null` for the `brands` field. This is because we haven't set up that relationship yet in the resolvers. Inside our resolver, we are going to create another query for the MusicAccessories` and have the value for the `brands` key be a filtered array of results for each brand. `js const resolvers = { Query: { accessories: () => musicAccessories, }, MusicAccessories: { // parent represents each music accessory brand: (parent) => { const isBrandInAccessory = (brand) => brand.id === parent.brandId; return musicBrands.find(isBrandInAccessory); }, }, }; ` When we run the query, this will be the final result: `graphql query Query { accessories { product brand { brandName } } } ` `json { "data": { "accessories": [ { "product": "NS Micro Violin Tuner Standard", "brands": [ { "brandName": "D'Addario" } ] }, { "product": "Standard Gong Stand", "brands": [ { "brandName": "Zildjian" } ] }, { "product": "Black Cymbal Mallets", "brands": [ { "brandName": "Zildjian" } ] }, { "product": "Classic Series XLR Microphone Cable", "brands": [ { "brandName": "D'Addario" } ] }, { "product": "Folding 5-Guitar Stand Standard", "brands": [ { "brandName": "Fender" } ] }, { "product": "Black Deluxe Drum Rug", "brands": [ { "brandName": "Zildjian" } ] } ] } } ` This single query makes it easy to access the data we need on the client side as compared to the REST API approach. If this were a REST API, then we would be dealing with multiple API calls and a Promise.all` which could get a little messy. You can find the entire code in this CodeSandbox example. A refactored approach using Data Loaders and Data Sources Even though our first approach does solve the issue of resolving nested queries, we still have an issue fetching the same data repeatedly. Let’s look at this example query: `graphql query MyAccessories { accessories { id brand { id brandName } } } ` If we take a look at the results, we are making additional queries for the brand each time we request the information. This leads to the N+1 problem in our current implementation. We can solve this issue by using Data Loaders and Data Sources. What are Data Loaders Data Loaders are used to batch and cache fetch requests. This allows us to fetch the same data and work with cached results, and reduce the number of API calls we have to make. To learn more about Data Loaders in GraphQL, please read this helpful article. How to setup a Data Source In this example, we will be using the following packages: - apollo-datasource - apollo-server-caching - dataloader We first need to create a BrandAccessoryDataSource` class which will simulate the fetching of our data. `js class BrandAccessoryDataSource extends DataSource { ... } ` We will then set up a constructor with a custom Dataloader. `js constructor() { super(); this.loader = new DataLoader((ids) => { if (!ids.length) { return musicAccessories; } return musicAccessories.filter((accessory) => ids.includes(accessory.id)); }); } ` Right below our constructor, we will set up the context and cache. `js initialize({ context, cache } = {}) { this.context = context; this.cache = cache || new InMemoryLRUCache(); } ` We then want to set up the error handling and cache keys for both the accessories and brands. To learn more about how caching works with GraphQL, please read through this article. `js didEncounterError(error) { throw new Error(There was an error loading data: ${error}`); } cacheKey(id) { return music-acc-${id}`; } cacheBrandKey(id) { return brand-acc-${id}`; } ` Next, we are going to set up an asynchronous function called get` which takes in an `id`. The goal of this function is to first check if there is anything in the cached results and if so return those cached results. Otherwise, we will set that data to the cache and return it. We will set the `ttl`(Time to Live in cache) value to 15 seconds. `js async get(id) { const cacheDoc = await this.cache.get(this.cacheKey(id)); if (cacheDoc) { return JSON.parse(cacheDoc); } const doc = await this.loader.load(id); this.cache.set(this.cacheKey(id), JSON.stringify(doc), { ttl: 15 }); return doc; } ` Below the get` function, we will create another asynchronous function called `getByBrand` which takes in a `brand`. This function will have a similar setup to the `get` function but will filter out the data by brand. `js async getByBrand(brand) { const cacheDoc = await this.cache.get(this.cacheBrandKey(brand.id)); if (cacheDoc) { return JSON.parse(cacheDoc); } const musicBrandAccessories = musicAccessories.filter( (accessory) => accessory.brandId === brand.id ); this.cache.set( this.cacheBrandKey(brand.id), JSON.stringify(musicBrandAccessories), { ttl: 15 } ); return musicBrandAccessories; } ` Setting up our schemas and resolvers The last part of this refactored example includes modifying the resolvers. We first need to add an accessory` key to our `Query` schema. `graphql type Query { brands: [Brand] accessory(id: Int): Accessory } ` Inside the resolver`, we will add the `accessories` key with a value for the function that returns the data source we created earlier. `js // this is the custom scalar type we added to the Accessory schema USCurrency, Query: { brands: () => musicBrands, accessory: (, { id }, context) => context.dataSources.brandAccessories.get(id), }, ` We also need to refactor our Brand` resolver to include the data source we set up earlier. `js Brand: { accessories: (brand, , context) => context.dataSources.brandAccessories.getByBrand(brand), }, ` Lastly, we need to modify our ApolloServer object to include the BrandAccessoryDataSource`. `js const server = new ApolloServer({ typeDefs, resolvers, dataSources: () => ({ brandAccessories: new BrandAccessoryDataSource() }), }); ` Here is the entire CodeSandbox example. When the server starts up, click on the Query your server` button and run the following query: `graphql query Query { brands { id brandName accessories { id product price } } } ` Resolving nested queries when microservices are involved Microservices is a type of architecture that will split up your software into smaller independent services. All of these smaller services can interact with a single API data layer. In this case, this data layer would be GraphQL. The client will interact directly with this data layer, and will consume API data from a single entry point. You would similarly resolve your nested queries as before because, at the end of the day, there are just functions. But now, this single API layer will reduce the number of requests made by the client because only the data layer will be called. This simplifies the data fetching experience on the client side. Conclusion In this article, we looked at a few code examples that explored different solutions on how to resolve nested queries in Apollo Server. The first approach involved creating custom resolvers and then using the filter` method to filter out music accessories by brand. We then refactored that example to use a custom DataLoader and Data Source to fix the "N+1 problem". Lastly, we briefly touched on how to approach this solution if microservices were involved. If you want to get started with Apollo Server and build your own nested queries and resolvers using these patterns, check out our serverless-apollo-contentful starter kit!...

Building a Multi-Response Streaming API with Node.js, Express, and React cover image

Building a Multi-Response Streaming API with Node.js, Express, and React

Introduction As web applications become increasingly complex and data-driven, efficient and effective data transfer methods become critically important. A streaming API that can send multiple responses to a single request can be a powerful tool for handling large amounts of data or for delivering real-time updates. In this article, we will guide you through the process of creating such an API. We will use video streaming as an illustrative example. With their large file sizes and the need for flexible, on-demand delivery, videos present a fitting scenario for showcasing the power of multi-response streaming APIs. The backend will be built with Node.js and Express, utilizing HTTP range requests to facilitate efficient data delivery in chunks. Next, we'll build a React front-end to interact with our streaming API. This front-end will handle both the display of the streamed video content and its download, offering users real-time progress updates. By the end of this walkthrough, you will have a working example of a multi-response streaming API, and you will be able to apply the principles learned to a wide array of use cases beyond video streaming. Let's jump right into it! Hands-On Implementing the Streaming API in Express In this section, we will dive into the server-side implementation, specifically our Node.js and Express application. We'll be implementing an API endpoint to deliver video content in a streaming fashion. Assuming you have already set up your Express server with TypeScript, we first need to define our video-serving route. We'll create a GET endpoint that, when hit, will stream a video file back to the client. Please make sure to install cors for handling cross-origin requests, dotenv for loading environment variables, and throttle for controlling the rate of data transfer. You can install these with the following command: ` yarn add cors dotenv throttle @types/cors @types/dotenv @types/throttle ` `typescript import cors from 'cors'; import 'dotenv/config'; import express, { Request, Response } from 'express'; import fs from 'fs'; import Throttle from 'throttle'; const app = express(); const port = 8000; app.use(cors()); app.get('/video', (req: Request, res: Response) => { // Video by Zlatin Georgiev from Pexels: https://www.pexels.com/video/15708449/ // For testing purposes - add the video in you static` folder const path = 'src/static/pexels-zlatin-georgiev-15708449 (2160p).mp4'; const stat = fs.statSync(path); const fileSize = stat.size; const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, '').split('-'); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = end - start + 1; const file = fs.createReadStream(path, { start, end }); const head = { 'Content-Range': bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/mp4', }; res.writeHead(206, head); file.pipe(res); } else { const head = { 'Content-Length': fileSize, 'Content-Type': 'video/mp4', }; res.writeHead(200, head); fs.createReadStream(path).pipe(res); } }); app.listen(port, () => { console.log(Server listening at ${process.env.SERVER_URL}:${port}`); }); ` In the code snippet above, we are implementing a basic video streaming server that responds to HTTP range requests. Here's a brief overview of the key parts: 1. File and Range Setup__: We start by determining the path to the video file and getting the file size. We also grab the range header from the request, which contains the range of bytes the client is requesting. 2. Range Requests Handling__: If a range is provided, we extract the start and end bytes from the range header, then create a read stream for that specific range. This allows us to stream a portion of the file rather than the entire thing. 3. Response Headers__: We then set up our response headers. In the case of a range request, we send back a '206 Partial Content' status along with information about the byte range and total file size. For non-range requests, we simply send back the total file size and the file type. 4. Data Streaming__: Finally, we pipe the read stream directly to the response. This step is where the video data actually gets sent back to the client. The use of pipe() here automatically handles backpressure, ensuring that data isn't read faster than it can be sent to the client. With this setup in place, our streaming server is capable of efficiently delivering large video files to the client in small chunks, providing a smoother user experience. Implementing the Download API in Express Now, let's add another endpoint to our Express application, which will provide more granular control over the data transfer process. We'll set up a GET endpoint for '/download', and within this endpoint, we'll handle streaming the video file to the client for download. `typescript app.get('/download', (req: Request, res: Response) => { // Again, for testing purposes - add the video in you static` folder const path = 'src/static/pexels-zlatin-georgiev-15708449 (2160p).mp4'; const stat = fs.statSync(path); const fileSize = stat.size; res.writeHead(200, { 'Content-Type': 'video/mp4', 'Content-Disposition': 'attachment; filename=video.mp4', 'Content-Length': fileSize, }); const readStream = fs.createReadStream(path); const throttle = new Throttle(1024 1024 * 5); // throttle to 5MB/sec - simulate lower speed readStream.pipe(throttle); throttle.on('data', (chunk) => { Console.log(Sent ${chunk.length} bytes to client.`); res.write(chunk); }); throttle.on('end', () => { console.log('File fully sent to client.'); res.end(); }); }); ` This endpoint has a similar setup to the video streaming endpoint, but it comes with a few key differences: 1. Response Headers__: Here, we include a 'Content-Disposition' header with an 'attachment' directive. This header tells the browser to present the file as a downloadable file named 'video.mp4'. 2. Throttling__: We use the 'throttle' package to limit the data transfer rate. Throttling can be useful for simulating lower-speed connections during testing, or for preventing your server from getting overwhelmed by data transfer operations. 3. Data Writing__: Instead of directly piping the read stream to the response, we attach 'data' and 'end' event listeners to the throttled stream. On the 'data' event, we manually write each chunk of data to the response, and on the 'end' event, we close the response. This implementation provides a more hands-on way to control the data transfer process. It allows for the addition of custom logic to handle events like pausing and resuming the data transfer, adding custom transformations to the data stream, or handling errors during transfer. Utilizing the APIs: A React Application Now that we have a server-side setup for video streaming and downloading, let's put these APIs into action within a client-side React application. Note that we'll be using Tailwind CSS for quick, utility-based styling in our components. Our React application will consist of a video player that uses the video streaming API, a download button to trigger the download API, and a progress bar to show the real-time download progress. First, let's define the Video Player component that will play the streamed video: `tsx import React from 'react'; const VideoPlayer: React.FC = () => { return ( Your browser does not support the video tag. ); }; export default VideoPlayer; ` In the above VideoPlayer component, we're using an HTML5 video tag to handle video playback. The src attribute of the source tag is set to the video endpoint of our Express server. When this component is rendered, it sends a request to our video API and starts streaming the video in response to the range requests that the browser automatically makes. Next, let's create the DownloadButton component that will handle the video download and display the download progress: `tsx import React, { useState } from 'react'; const DownloadButton: React.FC = () => { const [downloadProgress, setDownloadProgress] = useState(0); const handleDownload = async () => { try { const response = await fetch('http://localhost:8000/download'); const reader = response.body?.getReader(); if (!reader) { return; } const contentLength = +(response.headers?.get('Content-Length') || 0); let receivedLength = 0; let chunks = []; while (true) { const { done, value } = await reader.read(); if (done) { console.log('Download complete.'); const blob = new Blob(chunks, { type: 'video/mp4' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = 'video.mp4'; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); setDownloadProgress(100); break; } chunks.push(value); receivedLength += value.length; const progress = (receivedLength / contentLength) 100; setDownloadProgress(progress); } } catch (err) { console.error(err); } }; return ( Download Video {downloadProgress > 0 && downloadProgress Download progress: )} {downloadProgress === 100 && Download complete!} ); }; export default DownloadButton; ` In this DownloadButton component, when the download button is clicked, it sends a fetch request to our download API. It then uses a while loop to continually read chunks of data from the response as they arrive, updating the download progress until the download is complete. This is an example of more controlled handling of multi-response APIs where we are not just directly piping the data, but instead, processing it and manually sending it as a downloadable file. Bringing It All Together Let's now integrate these components into our main application component. `tsx import React from 'react'; import VideoPlayer from './components/VideoPlayer'; import DownloadButton from './components/DownloadButton'; function App() { return ( My Video Player ); } export default App; ` In this simple App component, we've included our VideoPlayer and DownloadButton components. It places the video player and download button on the screen in a neat, centered layout thanks to Tailwind CSS. Here is a summary of how our system operates: - The video player makes a request to our Express server as soon as it is rendered in the React application. Our server handles this request, reading the video file and sending back the appropriate chunks as per the range requested by the browser. This results in the video being streamed in our player. - When the download button is clicked, a fetch request is sent to our server's download API. This time, the server reads the file, but instead of just piping the data to the response, it controls the data sending process. It sends chunks of data and also logs the sent chunks for monitoring purposes. The React application collects these chunks and concatenates them, displaying the download progress in real-time. When all chunks are received, it compiles them into a Blob and triggers a download in the browser. This setup allows us to build a full-featured video streaming and downloading application with fine control over the data transmission process. To see this system in action, you can check out this video demo. Conclusion While the focus of this article was on video streaming and downloading, the principles we discussed here extend beyond just media files. The pattern of responding to HTTP range requests is common in various data-heavy applications, and understanding it can be a useful tool in your web development arsenal. Finally, remember that the code shown in this article is just a simple example to demonstrate the concepts. In a real-world application, you would want to add proper error handling, validation, and possibly some form of access control depending on your use case. I hope this article helps you in your journey as a developer. Building something yourself is the best way to learn, so don't hesitate to get your hands dirty and start coding!...

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools cover image

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools

In the ever-evolving world of web development, Nuxt.js has taken a monumental leap with the launch of Nuxt DevTools v1.0. More than just a set of tools, it's a game-changer—a faithful companion for developers. This groundbreaking release, available for all Nuxt projects and being defaulted from Nuxt v3.8 onwards, marks the beginning of a new era in developer tools. It's designed to simplify our development journey, offering unparalleled transparency, performance, and ease of use. Join me as we explore how Nuxt DevTools v1.0 is set to revolutionize our workflow, making development faster and more efficient than ever. What makes Nuxt DevTools so unique? Alright, let's start delving into the features that make this tool so amazing and unique. There are a lot, so buckle up! In-App DevTools The first thing that caught my attention is that breaking away from traditional browser extensions, Nuxt DevTools v1.0 is seamlessly integrated within your Nuxt app. This ensures universal compatibility across browsers and devices, offering a more stable and consistent development experience. This setup also means the tools are readily available in the app, making your work more efficient. It's a smart move from the usual browser extensions, making it a notable highlight. To use it you just need to press Shift + Option + D` (macOS) or `Shift + Alt + D` (Windows): With simple keystrokes, the Nuxt DevTools v1.0 springs to life directly within your app, ready for action. This integration eliminates the need to toggle between windows or panels, keeping your workflow streamlined and focused. The tools are not only easily accessible but also intelligently designed to enhance your productivity. Pages, Components, and Componsables View The Pages, Components, and Composables View in Nuxt DevTools v1.0 are a clear roadmap for your app. They help you understand how your app is built by simply showing its structure. It's like having a map that makes sense of your app's layout, making the complex parts of your code easier to understand. This is really helpful for new developers learning about the app and experienced developers working on big projects. Pages View lists all your app's pages, making it easier to move around and see how your site is structured. What's impressive is the live update capability. As you explore the DevTools, you can see the changes happening in real-time, giving you instant feedback on your app's behavior. Components View is like a detailed map of all the parts (components) your app uses, showing you how they connect and depend on each other. This helps you keep everything organized, especially in big projects. You can inspect components, change layouts, see their references, and filter them. By showcasing all the auto-imported composables, Nuxt DevTools provides a clear overview of the composables in use, including their source files. This feature brings much-needed clarity to managing composables within large projects. You can also see short descriptions and documentation links in some of them. Together, these features give you a clear picture of your app's layout and workings, simplifying navigation and management. Modules and Static Assets Management This aspect of the DevTools revolutionizes module management. It displays all registered modules, documentation, and repository links, making it easy to discover and install new modules from the community! This makes managing and expanding your app's capabilities more straightforward than ever. On the other hand, handling static assets like images and videos becomes a breeze. The tool allows you to preview and integrate these assets effortlessly within the DevTools environment. These features significantly enhance the ease and efficiency of managing your app's dynamic and static elements. The Runtime Config and Payload Editor The Runtime Config and Payload Editor in Nuxt DevTools make working with your app's settings and data straightforward. The Runtime Config lets you play with different configuration settings in real time, like adjusting settings on the fly and seeing the effects immediately. This is great for fine-tuning your app without guesswork. The Payload Editor is all about managing the data your app handles, especially data passed from server to client. It's like having a direct view and control over the data your app uses and displays. This tool is handy for seeing how changes in data impact your app, making it easier to understand and debug data-related issues. Open Graph Preview The Open Graph Preview in Nuxt DevTools is a feature I find incredibly handy and a real time-saver. It lets you see how your app will appear when shared on social media platforms. This tool is crucial for SEO and social media presence, as it previews the Open Graph tags (like images and descriptions) used when your app is shared. No more deploying first to check if everything looks right – you can now tweak and get instant feedback within the DevTools. This feature not only streamlines the process of optimizing for social media but also ensures your app makes the best possible first impression online. Timeline The Timeline feature in Nuxt DevTools is another standout tool. It lets you track when and how each part of your app (like composables) is called. This is different from typical performance tools because it focuses on the high-level aspects of your app, like navigation events and composable calls, giving you a more practical view of your app's operation. It's particularly useful for understanding the sequence and impact of events and actions in your app, making it easier to spot issues and optimize performance. This timeline view brings a new level of clarity to monitoring your app's behavior in real-time. Production Build Analyzer The Production Build Analyzer feature in Nuxt DevTools v1.0 is like a health check for your app. It looks at your app's final build and shows you how to make it better and faster. Think of it as a doctor for your app, pointing out areas that need improvement and helping you optimize performance. API Playground The API Playground in Nuxt DevTools v1.0 is like a sandbox where you can play and experiment with your app's APIs. It's a space where you can easily test and try out different things without affecting your main app. This makes it a great tool for trying out new ideas or checking how changes might work. Some other cool features Another amazing aspect of Nuxt DevTools is the embedded full-featured VS Code. It's like having your favorite code editor inside the DevTools, with all its powerful features and extensions. It's incredibly convenient for making quick edits or tweaks to your code. Then there's the Component Inspector. Think of it as your code's detective tool. It lets you easily pinpoint and understand which parts of your code are behind specific elements on your page. This makes identifying and editing components a breeze. And remember customization! Nuxt DevTools lets you tweak its UI to suit your style. This means you can set up the tools just how you like them, making your development environment more comfortable and tailored to your preferences. Conclusion In summary, Nuxt DevTools v1.0 marks a revolutionary step in web development, offering a comprehensive suite of features that elevate the entire development process. Features like live updates, easy navigation, and a user-friendly interface enrich the development experience. Each tool within Nuxt DevTools v1.0 is thoughtfully designed to simplify and enhance how developers build and manage their applications. In essence, Nuxt DevTools v1.0 is more than just a toolkit; it's a transformative companion for developers seeking to build high-quality web applications more efficiently and effectively. It represents the future of web development tools, setting new standards in developer experience and productivity....