A Next.js quickstart for generating and editing images with Gemini
Get started with native image generation and editing using Gemini 2.0 and Next.js
At a glance
- What is it?
- This repository is one of a family of small templates that show how to call a particular model capability from a modern web application, and the interesting part is how little it takes. The whole native image generation path is one model name, one configuration field that says the response may contain an image, and a loop over the parts of the response that distinguishes text from binary data.
- Who is it for?
- This quickstart fits a developer who wants a working reference for the model's image path rather than a library to build on, and who wants to see the request and the response shape before wiring the model into an application. It does not fit a production service, since the model identifier is an experimental one and the interface is deliberately plain.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 121 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the quickstart is
The repository is a quickstart for the image generation and editing capability of a particular model, built with the React web framework and a component library. The readme's framing is a Next.js quickstart for generating and editing images, and it links a hosted demo so that the thing can be seen before any of it is set up.
The four capabilities are listed at the top and they are worth reading as a design rather than as a list.
Generating images from a text prompt is the obvious one. Editing an image by uploading it and describing the change is the second, and it is the one that distinguishes this from a text-to-image endpoint. Conversation history is the third, and it is the one that makes the difference between a toy and a tool: because the model keeps the context of the conversation, you can say adjust the colour, and then make the lighting warmer, and then add a hat, and each instruction applies to the accumulated state rather than starting over. The fourth is downloading the result.
That third capability is really the point of the whole thing. An image model without conversation history is a random number generator with a prompt. With it, the interface is a conversation in which the image is the state, and the user is doing something much closer to directing than to describing.
The component library is worth a sentence too, because the model can return text as well as an image, and so the interface has to show both. The library is a set of reusable components built on a headless component toolkit and utility-first styling, which is the usual reason to pick one: the interface is assembled from accessible primitives rather than from a set of styled copies, and the styling is a set of design tokens rather than a stylesheet.
The project is under the permissive Apache 2.0 licence, the documentation link at the top of the repository metadata points at the model provider's own documentation for the feature, and the last push is recent.
The direct call, and the one line that makes it work
The readme leads with the case where you do not want the web application at all, and that is the right place to start, because the whole capability is about ten lines of request and response handling.
The client is constructed with the API key from the environment, and then the model is fetched with two things: a model identifier and a generation configuration whose response modalities include the image.
// Set responseModalities to include "Image" so the model can generate
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash-exp",
generationConfig: {
responseModalities: ["Text", "Image"]
}
});That configuration line is the whole trick, and the comment above it says so. A multimodal model has a setting that says which kinds of output it is allowed to produce, and a text-only setting means the model will not generate an image however good your prompt is. The most common first failure with this API is a developer who omits it and concludes that image generation does not work.
The identifier carries the experimental suffix, which is worth noticing for a second reason. Experimental identifiers are how a provider ships a capability before it commits to it, and they are the identifiers that change. A quickstart that pins one is a snapshot of one moment.
Then the call itself, which takes the contents and returns a response, and the part that every consumer has to get right is the iteration over the parts.
const response = await model.generateContent(contents);
for (const part of response.response.candidates[0].content.parts) {
// Based on the part type, either show the text or save the image
if (part.text) {
console.log(part.text);
} else if (part.inlineData) {
const imageData = part.inlineData.data;
const buffer = Buffer.from(imageData, "base64");
fs.writeFileSync("gemini-native-image.png", buffer);
console.log("Image saved as gemini-native-image.png");
}
}
} catch (error) {
console.error("Error generating content:", error);
}So the response is a list of candidates, each of which has content, and the content is a list of parts, and each part is either text or inline binary data. The client does not ask for an image and get an image; it gets a list of things and has to look at what each one is. That is the shape of a multimodal response, and it is why the example is written as a loop rather than as a single field access.
The binary data arrives base64 encoded, so it is decoded into a byte array and written to a file. The file name in the example is hard coded, which is fine for a demonstration and is the first thing a real application changes.
And the error handling is a plain try and catch, which is honest for a quickstart and is where a production implementation would add the retry and the logging that the provider's own documentation recommends.
The application, and the structure that comes with the generator
The repository layout is the modern one for this framework, and reading it tells you what is hand written and what the generator made.
There is an application directory, which in the current conventions of the framework means the router and the pages live there rather than in a separate routes directory. There is a components directory and, next to it, a configuration file for the component library's own generator, which records the style choices so that the generator produces components that match the rest of the application. There is a library directory for the shared code, a public directory for static assets, and an assets directory for images.
The configuration files at the root are the ones you would expect: a type configuration, the framework's own type declaration file, a styling configuration, the post-processor configuration, the linter configuration, and the framework's own configuration file. Two of these are worth naming because they are the interface between the application and the tools: the styling configuration and the post-processor configuration are how the utility-first classes in the components get compiled into the stylesheet that ships.
And there are two files that are pure archaeology. There is an empty module file at the root, which is a placeholder the generator writes and never uses, and there is a build file whose name does not match the project. The manifest's package name refers to a different capability entirely, a quickstart about structured data from documents, which is a copy-and-paste from a sibling template.
Neither file hurts anything, and both are worth knowing about, because a repository with leftover names from another project is a repository whose documentation is a starting point rather than a finished description of the code. If you fork this, the first thing to do is rename the package, because a name that describes a different feature will confuse every tool that reads it and every person who finds it.
The development script is also worth a glance, because it shows a framework that has grown a new bundler: the development command asks for the new bundler by name, while the build and the start commands do not mention it. That is a transitional state, and it is the sort of detail that a quickstart inherits from its template rather than choosing.
Configuration, secrets and the two deployment paths
The local setup is three steps, and the first one is the one that people get wrong.
The environment file is created by copying an example file, and the example file is in the repository and is not ignored by version control, so the copy is a safe operation: the template is committed, the file you edit is not.
The variable to set is the API key, and the readme links to the place to obtain one. The key is a single value, and it is the only secret in the project, which is a sign of how small the attack surface is.
Then the dependencies are installed and the development server is started, and the application is at the local port.
The deployment section gives two paths, and both of them handle the key in the way you would expect.
The first is a one-click deployment to a hosting platform, and the button is configured with the name of the environment variable so that the platform asks for the key as part of creating the project rather than the developer having to set it afterwards. That is a small piece of care in the link, and it is the difference between a deployment that works and one that fails at build time with a missing variable.
The second is a container, and the build and run commands are the conventional ones, with the port published and the key passed as an environment variable on the run command. The readme also gives the alternative of passing the environment file directly, which is the form you want when there is more than one variable or when the value is long enough that a command line is unwieldy.
And that is the right way round for a key. The container image does not contain the key; the key is supplied at run time, so the same image can be used in an environment where the key comes from a secret store rather than from a file. A quickstart that had baked the key into the image would be a quickstart everybody had to rebuild.
One thing the readme does not mention, and which is worth working out for yourself, is what the model provider's terms say about the images your users generate and where they are stored. A key in the environment is a key for a paid service, and the cost of a demo that a visitor can drive is somebody's problem.
The manifest, and what a quickstart family looks like
The manifest is short, and it is the last piece that tells you what kind of artefact this is.
The package is marked private, which means it is not published to a registry. That is correct for an application rather than a library, and it is the first sign that what you are looking at is a starting point for a deployable application rather than a dependency other people install.
The scripts are the four conventional ones: a development command, a build, a start of the built application, and a lint. And the dependencies begin with the provider's own client library for the generative interface, which is the modern package for it, at a version that is still moving.
The rest of the dependency list is the component library and the styling toolchain, which is what the interface is built from, and it is the part that would differ most from a sibling quickstart. Two quickstarts in the same family, one for image generation and one for structured extraction from documents, share the framework, the component library, the styling setup, the linter and the type configuration, and differ in the model client, the model identifier, the routes and the components.
That is worth pausing on, because it is how this kind of template is actually maintained. The repository is not written once and frozen; it is a copy of a base with the interesting parts replaced, and it inherits the base's configuration files, its scripts, its directory layout and occasionally its mistakes. The manifest with the wrong name is the visible symptom of that process.
So the value of a quickstart is not that it is a finished application. It is that it is a worked example with a specific model configuration, a specific response shape and a specific set of gotchas already solved, sitting on top of a base that somebody else is keeping current. Read the direct call section and copy the rest.
Which is, in the end, the honest summary of every quickstart: the model-specific part is the lesson, and the rest is scaffolding that you will replace.
Editorial conclusion
This quickstart fits a developer who wants a working reference for the model's image path rather than a library to build on, and who wants to see the request and the response shape before wiring the model into an application. It does not fit a production service, since the model identifier is an experimental one and the interface is deliberately plain. Verify first which model identifier the current documentation recommends, since an experimental identifier changes and the quickstart may lag, that the response modality configuration includes the image modality, because a text-only configuration is the most common reason nothing appears, and that your key stays in the environment and out of the image, since the container instructions pass it as an environment variable rather than baking it in.
Frequently asked questions
What does the Gemini image editing quickstart demonstrate?
Four things: generating an image from a text prompt, editing an uploaded image by describing the change in natural language, keeping the conversation history so that follow-up instructions apply to the accumulated state, and downloading the result. The web application wraps those four capabilities in a responsive interface built with a component library.
What is the most important line in the image generation request?
The generation configuration that sets the response modalities to include the image alongside text. A text-only configuration means the model will not generate an image however good the prompt is, and it is the most common reason a first attempt appears to do nothing.
How is the generated image returned and saved?
The response is a list of candidates whose content is a list of parts, and each part is either text or inline binary data. The example loops over the parts, prints the text and decodes the base64 binary data into a byte array, which is then written to a file. A consumer has to inspect the part type rather than assume a single field.
How do I deploy the quickstart application?
Either with the one-click deployment button, which is configured to ask for the API key as part of creating the project, or as a container, where the image is built, the port is published and the key is supplied at run time as an environment variable or through an environment file. The key is never part of the image.
Is there anything odd in the quickstart repository?
Two leftovers from the template it was copied from: a placeholder module file at the root, and a package name in the manifest that describes a different feature entirely, from a sibling quickstart about structured data. Neither breaks anything, but both mean the repository is a starting point rather than a finished description of itself.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/google-gemini-gemini-image-editing-nextjs-quickstart)Was this useful?