React has come a long way in terms of giving us JavaScript developers a quick and easy-to-use framework to build web applications without backend code. However, I recently encountered a use case where a full-stack web application was needed: a Jira ticketing system that allows users to create tickets, view ticket status, download reports, upload attachments, etc.
As it so happens, we really don’t need to stand up an entirely separate back-end to handle these requests server-side. Instead, we can simply utilize Next.js. The server side of Next.js can serve as a back-end for accessing and fetching data, which is exactly what we need for this use case.
Making requests to third-party services like Jira, AWS, etc. can bring out the dreaded and frustrating CORS error, especially if things are handled on the client side only. Therefore, a server side will prove to be essential. SSR or server-side rendering will help us in our example.
In this post, I will go over some technical details on how to work with server-side rendering in Next.js as well as some cool and helpful utilities to aid in file uploading, all done in React! We’ll take a dive into Static Generation, SSR, and Multi-Part Form Data Uploading using Next-Connect and Mutler.
Using the example I mentioned above, my goal is to give you an overview of what we can do with the server side of Next.js. Let’s get started!
Static Generation
Let’s say we need to fetch some Jira ticket data to render on a page. Sure, we can throw in the useEffect
hook, execute a fetch
, and set some state data. However, that’s on the client side, and it’s possible CORS errors might pop up, which is not ideal.
Here’s where server-side Next.js comes in. Next.js has a getStaticProps
function we can export that will generate the HTML at build time. This executes only on the server, preventing those pesky CORS errors. Here’s how to do it.
export const getStaticProps = async () => { Const allTickets = await getAllTickets() Return { Props: { allTickets } } }
That getAllTickets()
call will reside in your API and pass ticket results to whatever component you need them in as props.
const Tickets = ({allTickets}) => { Return ( <Container spacing={10}> {allTickets.issues.map(({key, status, assignee, displayName}) => ( <Paper key={key} > <Ticket {...{name: key, status, assignee, displayName}}/> </Paper> ) ) } ) }
If your data doesn’t change very often, this may be the way to go. For instance, in our case, we just want a list of open tickets at build time.
However, if you want access to data that changes often, say for a dashboard or something, you may opt to work with Server-Side Rendering, which brings us to our next topic.
Server-Side Rendering
The getServerSideProps
export function in server-side Next.js will be coded the same as the above getStaticProps
.
export const getServerSideProps = async () => { Const alldata = await getDashboardData() Return { Props: { allData } } }
This is all good when we are handling the fetching of data, but what if we need to post data somewhere?
In general, dealing with requests and passing along uploaded multi-part form data content can be tricky. The good news is that there are good utilities and libraries out there that work inside the Next.js middleware layer to make our lives a little easier.
Next-Connect
Speaking of handy Next.js utilities, let’s talk about Next-Connect. In a life without a utility like Next-Connect, we would need to hardcode switch cases or big if/else
statements in our routing functions to handle all the HTTP verb options.
Think of Express.js and Node. Next-Connect enables better handling of those standard HTTP verbs (GET
, POST
, PUT
, DELETE
, PATCH
), better error control, and just gives us an overall clean approach to our Next.js API routes. It even has support for Generics, making the TypeScript folks happy.
Next-Connect acts as a minimal router and middleware layer for Next.js.
For the processing of file uploads, there are quite a few npm packages out there that can help, but it definitely depends on if your business requirements need to stream files or if a temporary server save and removal is okay.
The example below uses Formidable but requires a disk save since it does not have the option for a memory save.
import formidable from "formidable"; import fs from "fs"; export const config = { api: { bodyParser: false } }; const post = async (req, res) => { const form = new formidable.IncomingForm(); form.parse(req, async function (err, fields, files) { await saveFile(files.file); return res.status(200).send("File successfully uploaded!"); }); }; const saveFile = async (file) => { const data = fs.readFileSync(file.path); fs.writeFileSync(`./public/${file.name}`, data); await fs.unlinkSync(file.path); return; };
The same goes for Multiparty and/or Busboy (other alternatives) in that they lack the option for a memory save.
Multer looks to be the only option for in-memory storage and passing along as a stream.
Multer
Multer works in sync with Next-Connect to handle any multi-part form data uploads in our routes.
Multer does two things. The first is to send a response based on the user request and to modify the request object before proceeding. In our file upload case, we want to modify the request object. The second is to supply us with two storage types: DiskStorage
and MemoryStorage
.
DiskStorage
, as the name implies, allows us to utilize the disk for storing files. In this case, all we would need to do is specify the destination and file name. However, if processing the file and sending it to 3rd party storage (say S3 for instance) is the requirement, then MemoryStorage
may be a better option. Multer is neat since it populates the req.files
object for you so they are ready for file consumption automatically.
Here’s a quick example of using Multer.
import nextConnect from 'next-connect'; import multer from 'multer'; const upload = multer({ // Disk Storage option storage: multer.diskStorage({ destination: './public/uploads', filename: (req, file, cb) => cb(null, file.originalname), }), }); //const storage = multer.memoryStorage() // Memory Storage option pass along as stream //const upload = multer({ storage: storage }) const apiRoute = nextConnect({ onError(error, req, res) { res.status(501).json({ error: `There was an error! ${error.message}` }); }, onNoMatch(req, res) { res.status(405).json({ error: `Method '${req.method}' Not Allowed` }); }, }); apiRoute.use(upload.array('files')); apiRoute.post((req, res) => { res.status(200).json({ data: 'Success' }); }); export default apiRoute; export const config = { api: { bodyParser: false, }, };
Next.js is set up nicely to give us a parsed request body, but only if the content type of the request is application/JSON. In this case, we don’t need to utilize JSON.parse(req.body)
since the parsing has already happened.
However, if we wanted to override this in our API route, we could just export a config object to modify the default configuration. An applicable use case would be to consume the body as a stream with a raw body. In that case, we can set the bodyParser
to false.
export const config = { api: { bodyParser: false, }, }
In the front-end React code where we are handling the UI for the file upload, we need to ensure we specify the appropriate headers in our case. This value is required when we use forms that have a file input type element like the standard file upload button.
const onChange = async () => { const formData = new FormData(); Object.entries(data).forEach(([key, value]) => { formData.set(key, value); }); const config = { headers: { 'content-type': 'multipart/form-data' }, }; const response = await axios.post('/api/upload', formData, config); };
And that is it! Next.js and Multer work seamlessly and allow us to handle whatever comes our way in terms of API routing and file uploads.
In Conclusion…
The power of Next.js and data fetching on the server side is a real nice-to-have in our front-end developer toolkit. As a full stack developer, if you have been reliant on having to stand up an entirely separate backend for simple API, data-fetch-like requests, these features of the server side of Next.js may come in handy and keep your application as lightweight as possible.
Whether you’re a front-end dev or full-stack, I hope this post has shown you the benefit of server-side Next.js – from Static Generation to SSR to Multi-Part Form Data Uploading. Give it a try sometime, let me know what you think, and be sure to subscribe to the Keyhole Dev Blog.
Happy coding!